import { humanizedDateTime, favsToHotswap, getMessageTimeStamp, dragElement, isMobile, initRossMods, } from "./scripts/RossAscends-mods.js";
import { userStatsHandler, statMesProcess, initStats } from './scripts/stats.js';
import {
    generateKoboldWithStreaming,
    kai_settings,
    loadKoboldSettings,
    formatKoboldUrl,
    getKoboldGenerationData,
    kai_flags,
    setKoboldFlags,
} from "./scripts/kai-settings.js";

import {
    textgenerationwebui_settings,
    loadTextGenSettings,
    generateTextGenWithStreaming,
    getTextGenGenerationData,
    formatTextGenURL,
    getTextGenUrlSourceId,
    isMancer,
    isAphrodite,
    textgen_types,
    textgenerationwebui_banned_in_macros,
} from "./scripts/textgen-settings.js";

import {
    world_info,
    getWorldInfoPrompt,
    getWorldInfoSettings,
    setWorldInfoSettings,
    world_names,
    importEmbeddedWorldInfo,
    checkEmbeddedWorld,
    setWorldInfoButtonClass,
    importWorldInfo,
} from "./scripts/world-info.js";

import {
    groups,
    selected_group,
    saveGroupChat,
    getGroups,
    generateGroupWrapper,
    deleteGroup,
    is_group_generating,
    resetSelectedGroup,
    select_group_chats,
    regenerateGroup,
    group_generation_id,
    getGroupChat,
    renameGroupMember,
    createNewGroupChat,
    getGroupPastChats,
    getGroupAvatar,
    openGroupChat,
    editGroup,
    deleteGroupChat,
    renameGroupChat,
    importGroupChat,
    getGroupBlock,
    getGroupChatNames,
    getGroupCharacterCards,
    getGroupDepthPrompts,
} from "./scripts/group-chats.js";

import {
    collapseNewlines,
    loadPowerUserSettings,
    playMessageSound,
    fixMarkdown,
    power_user,
    persona_description_positions,
    loadMovingUIState,
    getCustomStoppingStrings,
    MAX_CONTEXT_DEFAULT,
    renderStoryString,
    sortEntitiesList,
    registerDebugFunction,
    ui_mode,
    switchSimpleMode,
} from "./scripts/power-user.js";

import {
    setOpenAIMessageExamples,
    setOpenAIMessages,
    setupChatCompletionPromptManager,
    prepareOpenAIMessages,
    sendOpenAIRequest,
    loadOpenAISettings,
    setOpenAIOnlineStatus,
    oai_settings,
    is_get_status_openai,
    openai_messages_count,
    chat_completion_sources,
    getChatCompletionModel,
} from "./scripts/openai.js";

import {
    generateNovelWithStreaming,
    getNovelGenerationData,
    getKayraMaxContextTokens,
    getNovelTier,
    loadNovelPreset,
    loadNovelSettings,
    nai_settings,
    adjustNovelInstructionPrompt,
    loadNovelSubscriptionData,
} from "./scripts/nai-settings.js";

import {
    createNewBookmark,
    showBookmarksButtons,
    createBranch,
} from "./scripts/bookmarks.js";

import {
    horde_settings,
    loadHordeSettings,
    generateHorde,
    checkHordeStatus,
    getHordeModels,
    adjustHordeGenerationParams,
    MIN_LENGTH,
} from "./scripts/horde.js";

import {
    debounce,
    delay,
    restoreCaretPosition,
    saveCaretPosition,
    trimToEndSentence,
    countOccurrences,
    isOdd,
    sortMoments,
    timestampToMoment,
    download,
    isDataURL,
    getCharaFilename,
    isDigitsOnly,
    PAGINATION_TEMPLATE,
    waitUntilCondition,
    escapeRegex,
    resetScrollHeight,
    onlyUnique,
} from "./scripts/utils.js";

import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, processExtensionHelpers, registerExtensionHelper, renderExtensionTemplate, runGenerationInterceptors, saveMetadataDebounced } from "./scripts/extensions.js";
import { COMMENT_NAME_DEFAULT, executeSlashCommands, getSlashCommandsHelp, registerSlashCommand } from "./scripts/slash-commands.js";
import {
    tag_map,
    tags,
    loadTagsSettings,
    printTagFilters,
    getTagsList,
    appendTagToList,
    createTagMapFromList,
    renameTagKey,
    importTags,
    tag_filter_types,
} from "./scripts/tags.js";
import {
    SECRET_KEYS,
    readSecretState,
    secret_state,
    writeSecret
} from "./scripts/secrets.js";
import { EventEmitter } from './lib/eventemitter.js';
import { markdownExclusionExt } from "./scripts/showdown-exclusion.js";
import { NOTE_MODULE_NAME, initAuthorsNote, metadata_keys, setFloatingPrompt, shouldWIAddPrompt } from "./scripts/authors-note.js";
import { getDeviceInfo } from "./scripts/RossAscends-mods.js";
import { registerPromptManagerMigration } from "./scripts/PromptManager.js";
import { getRegexedString, regex_placement } from "./scripts/extensions/regex/engine.js";
import { FILTER_TYPES, FilterHelper } from "./scripts/filters.js";
import { getCfgPrompt, getGuidanceScale, initCfg } from "./scripts/cfg-scale.js";
import {
    force_output_sequence,
    formatInstructModeChat,
    formatInstructModePrompt,
    formatInstructModeExamples,
    getInstructStoppingSequences,
    autoSelectInstructPreset,
    formatInstructModeSystemPrompt,
} from "./scripts/instruct-mode.js";
import { applyLocale } from "./scripts/i18n.js";
import { getTokenCount, getTokenizerModel, initTokenizers, saveTokenCache } from "./scripts/tokenizers.js";
import { initPersonas, selectCurrentPersona, setPersonaDescription } from "./scripts/personas.js";
import { getBackgrounds, initBackgrounds } from "./scripts/backgrounds.js";

//exporting functions and vars for mods
export {
    Generate,
    getSettings,
    saveSettings,
    saveSettingsDebounced,
    printMessages,
    clearChat,
    getChat,
    getCharacters,
    callPopup,
    substituteParams,
    sendSystemMessage,
    addOneMessage,
    deleteLastMessage,
    resetChatState,
    select_rm_info,
    setCharacterId,
    setCharacterName,
    replaceCurrentChat,
    setOnlineStatus,
    checkOnlineStatus,
    setEditedMessageId,
    setSendButtonState,
    selectRightMenuWithAnimation,
    openCharacterChat,
    saveChat,
    messageFormatting,
    getExtensionPrompt,
    getExtensionPromptByName,
    showSwipeButtons,
    hideSwipeButtons,
    changeMainAPI,
    setGenerationProgress,
    updateChatMetadata,
    scrollChatToBottom,
    isStreamingEnabled,
    getThumbnailUrl,
    getStoppingStrings,
    getStatus,
    reloadMarkdownProcessor,
    getCurrentChatId,
    chat,
    this_chid,
    selected_button,
    menu_type,
    settings,
    characters,
    online_status,
    main_api,
    api_server,
    system_messages,
    nai_settings,
    token,
    name1,
    name2,
    is_send_press,
    api_server_textgenerationwebui,
    max_context,
    chat_metadata,
    streamingProcessor,
    default_avatar,
    system_message_types,
    talkativeness_default,
    default_ch_mes,
    extension_prompt_types,
    mesForShowdownParse,
    printCharacters,
    isOdd,
    countOccurrences
}

// Allow target="_blank" in links
DOMPurify.addHook('afterSanitizeAttributes', function (node) {
    if ('target' in node) {
        node.setAttribute('target', '_blank');
        node.setAttribute('rel', 'noopener');
    }
});

// API OBJECT FOR EXTERNAL WIRING
window["SillyTavern"] = {};

// Event source init
export const event_types = {
    EXTRAS_CONNECTED: 'extras_connected',
    MESSAGE_SWIPED: 'message_swiped',
    MESSAGE_SENT: 'message_sent',
    MESSAGE_RECEIVED: 'message_received',
    MESSAGE_EDITED: 'message_edited',
    MESSAGE_DELETED: 'message_deleted',
    IMPERSONATE_READY: 'impersonate_ready',
    CHAT_CHANGED: 'chat_id_changed',
    GENERATION_STOPPED: 'generation_stopped',
    EXTENSIONS_FIRST_LOAD: 'extensions_first_load',
    SETTINGS_LOADED: 'settings_loaded',
    SETTINGS_UPDATED: 'settings_updated',
    GROUP_UPDATED: 'group_updated',
    MOVABLE_PANELS_RESET: 'movable_panels_reset',
    SETTINGS_LOADED_BEFORE: 'settings_loaded_before',
    SETTINGS_LOADED_AFTER: 'settings_loaded_after',
    CHATCOMPLETION_SOURCE_CHANGED: 'chatcompletion_source_changed',
    CHATCOMPLETION_MODEL_CHANGED: 'chatcompletion_model_changed',
    OAI_BEFORE_CHATCOMPLETION: 'oai_before_chatcompletion',
    OAI_PRESET_CHANGED_BEFORE: 'oai_preset_changed_before',
    OAI_PRESET_CHANGED_AFTER: 'oai_preset_changed_after',
    WORLDINFO_SETTINGS_UPDATED: 'worldinfo_settings_updated',
    CHARACTER_EDITED: 'character_edited',
    USER_MESSAGE_RENDERED: 'user_message_rendered',
    CHARACTER_MESSAGE_RENDERED: 'character_message_rendered',
    FORCE_SET_BACKGROUND: 'force_set_background',
    CHAT_DELETED: 'chat_deleted',
    GROUP_CHAT_DELETED: 'group_chat_deleted',
}

export const eventSource = new EventEmitter();

// Check for override warnings every 5 seconds...
setInterval(displayOverrideWarnings, 5000);
// ...or when the chat changes
eventSource.on(event_types.SETTINGS_LOADED, () => { settingsReady = true; });
eventSource.on(event_types.CHAT_CHANGED, displayOverrideWarnings);
eventSource.on(event_types.MESSAGE_RECEIVED, processExtensionHelpers);
eventSource.on(event_types.MESSAGE_SENT, processExtensionHelpers);

hljs.addPlugin({ "before:highlightElement": ({ el }) => { el.textContent = el.innerText } });

// Markdown converter
let mesForShowdownParse; //intended to be used as a context to compare showdown strings against
let converter;
reloadMarkdownProcessor();

// array for prompt token calculations
console.debug('initializing Prompt Itemization Array on Startup');
let itemizedPrompts = [];

export const systemUserName = "SillyTavern System";
let default_user_name = "User";
let name1 = default_user_name;
let name2 = "SillyTavern System";
let chat = [];
let safetychat = [
    {
        name: systemUserName,
        is_user: false,
        create_date: 0,
        mes: "You deleted a character/chat and arrived back here for safety reasons! Pick another character!",
    },
];
let chatSaveTimeout;
let importFlashTimeout;
export let isChatSaving = false;
let chat_create_date = 0;
let firstRun = false;
let settingsReady = false;
let currentVersion = "0.0.0";

const default_ch_mes = "Hello";
let count_view_mes = 0;
let generatedPromptCache = "";
let generation_started = new Date();
let characters = [];
let this_chid;
let saveCharactersPage = 0;
const default_avatar = "img/ai4.png";
export const system_avatar = "img/five.png";
export const comment_avatar = "img/quill.png";
export let CLIENT_VERSION = 'SillyTavern:UNKNOWN:Cohee#1207'; // For Horde header
let optionsPopper = Popper.createPopper(document.getElementById('options_button'), document.getElementById('options'), {
    placement: 'top-start'
});
let exportPopper = Popper.createPopper(document.getElementById('export_button'), document.getElementById('export_format_popup'), {
    placement: 'left'
});
let rawPromptPopper = Popper.createPopper(document.getElementById('dialogue_popup'), document.getElementById('rawPromptPopup'), {
    placement: 'right'
});

let dialogueResolve = null;
let chat_metadata = {};
let streamingProcessor = null;
let crop_data = undefined;
let is_delete_mode = false;
let fav_ch_checked = false;
let scrollLock = false;

const durationSaveEdit = 1000;
const saveSettingsDebounced = debounce(() => saveSettings(), durationSaveEdit);
export const saveCharacterDebounced = debounce(() => $("#create_button").trigger('click'), durationSaveEdit);

const system_message_types = {
    HELP: "help",
    WELCOME: "welcome",
    GROUP: "group",
    EMPTY: "empty",
    GENERIC: "generic",
    BOOKMARK_CREATED: "bookmark_created",
    BOOKMARK_BACK: "bookmark_back",
    NARRATOR: "narrator",
    COMMENT: "comment",
    SLASH_COMMANDS: "slash_commands",
    FORMATTING: "formatting",
    HOTKEYS: "hotkeys",
    MACROS: "macros",
};

const extension_prompt_types = {
    IN_PROMPT: 0,
    IN_CHAT: 1,
    BEFORE_PROMPT: 2
};

export const MAX_INJECTION_DEPTH = 1000;

let system_messages = {};

function getSystemMessages() {
    system_messages = {
        help: {
            name: systemUserName,
            force_avatar: system_avatar,
            is_user: false,
            is_system: true,
            mes: renderTemplate("help"),
        },
        slash_commands: {
            name: systemUserName,
            force_avatar: system_avatar,
            is_user: false,
            is_system: true,
            mes: '',
        },
        hotkeys: {
            name: systemUserName,
            force_avatar: system_avatar,
            is_user: false,
            is_system: true,
            mes: renderTemplate("hotkeys"),
        },
        formatting: {
            name: systemUserName,
            force_avatar: system_avatar,
            is_user: false,
            is_system: true,
            mes: renderTemplate("formatting"),
        },
        macros: {
            name: systemUserName,
            force_avatar: system_avatar,
            is_user: false,
            is_system: true,
            mes: renderTemplate("macros"),
        },
        welcome:
        {
            name: systemUserName,
            force_avatar: system_avatar,
            is_user: false,
            is_system: true,
            mes: renderTemplate("welcome"),
        },
        group: {
            name: systemUserName,
            force_avatar: system_avatar,
            is_user: false,
            is_system: true,
            is_group: true,
            mes: "Group chat created. Say 'Hi' to lovely people!",
        },
        empty: {
            name: systemUserName,
            force_avatar: system_avatar,
            is_user: false,
            is_system: true,
            mes: "No one hears you. <b>Hint&#58;</b> add more members to the group!",
        },
        generic: {
            name: systemUserName,
            force_avatar: system_avatar,
            is_user: false,
            is_system: true,
            mes: "Generic system message. User `text` parameter to override the contents",
        },
        bookmark_created: {
            name: systemUserName,
            force_avatar: system_avatar,
            is_user: false,
            is_system: true,
            mes: `Bookmark created! Click here to open the bookmark chat: <a class="bookmark_link" file_name="{0}" href="javascript:void(null);">{1}</a>`,
        },
        bookmark_back: {
            name: systemUserName,
            force_avatar: system_avatar,
            is_user: false,
            is_system: true,
            mes: `Click here to return to the previous chat: <a class="bookmark_link" file_name="{0}" href="javascript:void(null);">Return</a>`,
        },
    };
}

// Register configuration migrations
registerPromptManagerMigration();

$(document).ajaxError(function myErrorHandler(_, xhr) {
    if (xhr.status == 403) {
        toastr.warning(
            "doubleCsrf errors in console are NORMAL in this case. If you want to run ST in multiple tabs, start the server with --disableCsrf option.",
            "Looks like you've opened SillyTavern in another browser tab",
            { timeOut: 0, extendedTimeOut: 0, preventDuplicates: true },
        );
    }
});

function getUrlSync(url, cache = true) {
    return $.ajax({
        type: "GET",
        url: url,
        cache: cache,
        async: false
    }).responseText;
}

const templateCache = {};

export function renderTemplate(templateId, templateData = {}, sanitize = true, localize = true, fullPath = false) {
    try {
        const pathToTemplate = fullPath ? templateId : `/scripts/templates/${templateId}.html`;
        const templateContent = (pathToTemplate in templateCache) ? templateCache[pathToTemplate] : getUrlSync(pathToTemplate);
        templateCache[pathToTemplate] = templateContent;
        const template = Handlebars.compile(templateContent);
        let result = template(templateData);

        if (sanitize) {
            result = DOMPurify.sanitize(result);
        }

        if (localize) {
            result = applyLocale(result);
        }

        return result;
    } catch (err) {
        console.error("Error rendering template", templateId, templateData, err);
        toastr.error("Check the DevTools console for more information.", "Error rendering template");
    }
}

async function getClientVersion() {
    try {
        const response = await fetch('/version');
        const data = await response.json();
        CLIENT_VERSION = data.agent;
        let displayVersion = `SillyTavern ${data.pkgVersion}`;
        currentVersion = data.pkgVersion;

        if (data.gitRevision && data.gitBranch) {
            displayVersion += ` '${data.gitBranch}' (${data.gitRevision})`;
        }

        $('#version_display').text(displayVersion);
        $('#version_display_welcome').text(displayVersion);
    } catch (err) {
        console.error("Couldn't get client version", err);
    }
}

function reloadMarkdownProcessor(render_formulas = false) {
    if (render_formulas) {
        converter = new showdown.Converter({
            emoji: true,
            underline: true,
            tables: true,
            parseImgDimensions: true,
            extensions: [
                showdownKatex(
                    {
                        delimiters: [
                            { left: '$$', right: '$$', display: true, asciimath: false },
                            { left: '$', right: '$', display: false, asciimath: true },
                        ]
                    }
                )],
        });
    }
    else {
        converter = new showdown.Converter({
            emoji: true,
            literalMidWordUnderscores: true,
            parseImgDimensions: true,
            tables: true,
        });
    }

    // Inject the dinkus extension after creating the converter
    // Maybe move this into power_user init?
    setTimeout(() => {
        if (power_user) {
            converter.addExtension(markdownExclusionExt(), 'exclusion');
        }
    }, 1)

    return converter;
}

function getCurrentChatId() {
    if (selected_group) {
        return groups.find(x => x.id == selected_group)?.chat_id;
    }
    else if (this_chid) {
        return characters[this_chid].chat;
    }
}

const talkativeness_default = 0.5;
export const depth_prompt_depth_default = 4;
const per_page_default = 50;

var is_advanced_char_open = false;

var menu_type = ""; //what is selected in the menu
var selected_button = ""; //which button pressed
//create pole save
let create_save = {
    name: "",
    description: "",
    creator_notes: "",
    post_history_instructions: "",
    character_version: "",
    system_prompt: "",
    tags: "",
    creator: "",
    personality: "",
    first_message: "",
    avatar: "",
    scenario: "",
    mes_example: "",
    world: "",
    talkativeness: talkativeness_default,
    alternate_greetings: [],
    depth_prompt_prompt: '',
    depth_prompt_depth: depth_prompt_depth_default,
};

//animation right menu
let animation_duration = 125;
let animation_easing = "ease-in-out";
let popup_type = "";
let chat_file_for_del = "";
let online_status = "no_connection";

let api_server = "";
let api_server_textgenerationwebui = "";
//var interval_timer = setInterval(getStatus, 2000);
//let interval_timer_novel = setInterval(getStatusNovel, 90000);
let is_get_status = false;
let is_get_status_novel = false;
let is_api_button_press = false;
let is_api_button_press_novel = false;

let is_send_press = false; //Send generation

let this_del_mes = 0;

//message editing and chat scroll position persistence
var this_edit_mes_text = "";
var this_edit_mes_chname = "";
var this_edit_mes_id;
var scroll_holder = 0;
var is_use_scroll_holder = false;

//settings
var settings;
export let koboldai_settings;
export let koboldai_setting_names;
var preset_settings = "gui";
export let user_avatar = "you.png";
export var amount_gen = 80; //default max length of AI generated responses
var max_context = 2048;

var message_already_generated = "";

var swipes = true;
let extension_prompts = {};

var main_api;// = "kobold";
//novel settings
export let novelai_settings;
export let novelai_setting_names;
let abortController;

//css
var css_mes_bg = $('<div class="mes"></div>').css("background");
var css_send_form_display = $("<div id=send_form></div>").css("display");
let generate_loop_counter = 0;
const MAX_GENERATION_LOOPS = 5;

var kobold_horde_model = "";

let token;

var PromptArrayItemForRawPromptDisplay;

export let active_character = "";
export let active_group = "";
export const entitiesFilter = new FilterHelper(debounce(printCharacters, 100));

export function getRequestHeaders() {
    return {
        "Content-Type": "application/json",
        "X-CSRF-Token": token,
    };
}

$.ajaxPrefilter((options, originalOptions, xhr) => {
    xhr.setRequestHeader("X-CSRF-Token", token);
});

async function firstLoadInit() {
    try {
        const tokenResponse = await fetch('/csrf-token');
        const tokenData = await tokenResponse.json();
        token = tokenData.token;
    } catch {
        toastr.error("Couldn't get CSRF token. Please refresh the page.", "Error", { timeOut: 0, extendedTimeOut: 0, preventDuplicates: true });
        throw new Error("Initialization failed");
    }

    getSystemMessages();
    sendSystemMessage(system_message_types.WELCOME);
    await readSecretState();
    await getClientVersion();
    await getSettings("def");
    await getUserAvatars();
    await getCharacters();
    await getBackgrounds();
    await initTokenizers();
    initBackgrounds();
    initAuthorsNote();
    initPersonas();
    initRossMods();
    initStats();
    initCfg();
    doDailyExtensionUpdatesCheck();
}

function checkOnlineStatus() {
    ///////// REMOVED LINES THAT DUPLICATE RA_CHeckOnlineStatus FEATURES
    if (online_status == "no_connection") {
        $("#online_status_indicator2").css("background-color", "red");  //Kobold
        $("#online_status_text2").html("No connection...");
        $("#online_status_indicator_horde").css("background-color", "red");  //Kobold Horde
        $("#online_status_text_horde").html("No connection...");
        $("#online_status_indicator3").css("background-color", "red");  //Novel
        $("#online_status_text3").html("No connection...");
        $(".online_status_indicator4").css("background-color", "red");  //OAI / ooba
        $(".online_status_text4").html("No connection...");
        is_get_status = false;
        is_get_status_novel = false;
        setOpenAIOnlineStatus(false);
    } else {
        $("#online_status_indicator2").css("background-color", "green"); //kobold
        $("#online_status_text2").html(online_status);
        $("#online_status_indicator_horde").css("background-color", "green");  //Kobold Horde
        $("#online_status_text_horde").html(online_status);
        $("#online_status_indicator3").css("background-color", "green"); //novel
        $("#online_status_text3").html(online_status);
        $(".online_status_indicator4").css("background-color", "green"); //OAI / ooba
        $(".online_status_text4").html(online_status);
    }
}

export function setActiveCharacter(character) {
    active_character = character;
}

export function setActiveGroup(group) {
    active_group = group;
}

async function getStatus() {
    if (is_get_status) {
        if (main_api == "koboldhorde") {
            try {
                const hordeStatus = await checkHordeStatus();
                online_status = hordeStatus ? 'Connected' : 'no_connection';
                resultCheckStatus();
            }
            catch {
                online_status = "no_connection";
                resultCheckStatus();
            }

            return;
        }

        jQuery.ajax({
            type: "POST", //
            url: "/getstatus", //
            data: JSON.stringify({
                api_server: main_api == "kobold" ? api_server : api_server_textgenerationwebui,
                main_api: main_api,
                use_mancer: main_api == "textgenerationwebui" ? isMancer() : false,
                use_aphrodite: main_api == "textgenerationwebui" ? isAphrodite() : false,
            }),
            beforeSend: function () { },
            cache: false,
            dataType: "json",
            crossDomain: true,
            contentType: "application/json",
            //processData: false,
            success: function (data) {
                online_status = data.result;
                if (online_status == undefined) {
                    online_status = "no_connection";
                }

                // Determine instruct mode preset
                autoSelectInstructPreset(online_status);

                // determine if we can use stop sequence and streaming
                if (main_api === "kobold" || main_api === "koboldhorde") {
                    setKoboldFlags(data.version, data.koboldVersion);
                }

                // We didn't get a 200 status code, but the endpoint has an explanation. Which means it DID connect, but I digress.
                if (online_status == "no_connection" && data.response) {
                    toastr.error(data.response, "API Error", { timeOut: 5000, preventDuplicates: true })
                }

                //console.log(online_status);
                resultCheckStatus();
            },
            error: function (jqXHR, exception) {
                console.log(exception);
                console.log(jqXHR);
                online_status = "no_connection";

                resultCheckStatus();
            },
        });
    } else {
        if (is_get_status_novel != true && is_get_status_openai != true) {
            online_status = "no_connection";
        }
    }
}

function resultCheckStatus() {
    is_api_button_press = false;
    checkOnlineStatus();
    $("#api_loading").css("display", "none");
    $("#api_button").css("display", "inline-block");
    $("#api_loading_textgenerationwebui").css("display", "none");
    $("#api_button_textgenerationwebui").css("display", "inline-block");
}

export function selectCharacterById(id) {
    if (characters[id] == undefined) {
        return;
    }

    if (isChatSaving) {
        toastr.info("Please wait until the chat is saved before switching characters.", "Your chat is still saving...");
        return;
    }

    if (selected_group && is_group_generating) {
        return;
    }

    if (selected_group || this_chid !== id) {
        //if clicked on a different character from what was currently selected
        if (!is_send_press) {
            cancelTtsPlay();
            resetSelectedGroup();
            this_edit_mes_id = undefined;
            selected_button = "character_edit";
            this_chid = id;
            clearChat();
            chat.length = 0;
            chat_metadata = {};
            getChat();
        }
    } else {
        //if clicked on character that was already selected
        selected_button = "character_edit";
        select_selected_character(this_chid);
    }
}

function getCharacterBlock(item, id) {
    let this_avatar = default_avatar;
    if (item.avatar != "none") {
        this_avatar = getThumbnailUrl('avatar', item.avatar);
    }
    // Populate the template
    const template = $('#character_template .character_select').clone();
    template.attr({ 'chid': id, 'id': `CharID${id}` });
    template.find('img').attr('src', this_avatar);
    template.find('.avatar').attr('title', item.avatar);
    template.find('.ch_name').text(item.name);
    if (power_user.show_card_avatar_urls) {
        template.find('.ch_avatar_url').text(item.avatar);
    }
    template.find('.ch_fav_icon').css("display", 'none');
    template.toggleClass('is_fav', item.fav || item.fav == 'true');
    template.find('.ch_fav').val(item.fav);

    const description = item.data?.creator_notes?.split('\n', 1)[0] || '';
    if (description) {
        template.find('.ch_description').text(description);
    }
    else {
        template.find('.ch_description').hide();
    }

    const version = item.data?.character_version || '';
    if (version) {
        template.find('.character_version').text(version);
    }
    else {
        template.find('.character_version').hide();
    }

    // Display inline tags
    const tags = getTagsList(item.avatar);
    const tagsElement = template.find('.tags');
    tags.forEach(tag => appendTagToList(tagsElement, tag, {}));

    // Add to the list
    return template;
}

async function printCharacters(fullRefresh = false) {
    if (fullRefresh) {
        saveCharactersPage = 0;
        printTagFilters(tag_filter_types.character);
        printTagFilters(tag_filter_types.group_member);

        await delay(1);
        displayOverrideWarnings();
    }

    const storageKey = 'Characters_PerPage';
    $("#rm_print_characters_pagination").pagination({
        dataSource: getEntitiesList({ doFilter: true }),
        pageSize: Number(localStorage.getItem(storageKey)) || per_page_default,
        sizeChangerOptions: [10, 25, 50, 100, 250, 500, 1000],
        pageRange: 1,
        pageNumber: saveCharactersPage || 1,
        position: 'top',
        showPageNumbers: false,
        showSizeChanger: true,
        prevText: '<',
        nextText: '>',
        formatNavigator: PAGINATION_TEMPLATE,
        showNavigator: true,
        callback: function (data) {
            $("#rm_print_characters_block").empty();
            for (const i of data) {
                if (i.type === 'character') {
                    $("#rm_print_characters_block").append(getCharacterBlock(i.item, i.id));
                }
                if (i.type === 'group') {
                    $("#rm_print_characters_block").append(getGroupBlock(i.item));
                }
            }
        },
        afterSizeSelectorChange: function (e) {
            localStorage.setItem(storageKey, e.target.value);
        },
        afterPaging: function (e) {
            saveCharactersPage = e;
        },
        afterRender: function () {
            $('#rm_print_characters_block').scrollTop(0);
        },
    });

    favsToHotswap();
}

export function getEntitiesList({ doFilter } = {}) {
    let entities = [];
    entities.push(...characters.map((item, index) => ({ item, id: index, type: 'character' })));
    entities.push(...groups.map((item) => ({ item, id: item.id, type: 'group' })));

    if (doFilter) {
        entities = entitiesFilter.applyFilters(entities);
    }

    sortEntitiesList(entities);
    return entities;
}

async function getOneCharacter(avatarUrl) {
    const response = await fetch("/getonecharacter", {
        method: "POST",
        headers: getRequestHeaders(),
        body: JSON.stringify({
            avatar_url: avatarUrl,
        }),
    });

    if (response.ok) {
        const getData = await response.json();
        getData['name'] = DOMPurify.sanitize(getData['name']);
        getData['chat'] = String(getData['chat']);

        const indexOf = characters.findIndex(x => x.avatar === avatarUrl);

        if (indexOf !== -1) {
            characters[indexOf] = getData;
        } else {
            toastr.error(`Character ${avatarUrl} not found in the list`, "Error", { timeOut: 5000, preventDuplicates: true });
        }
    }
}

async function getCharacters() {
    var response = await fetch("/getcharacters", {
        method: "POST",
        headers: getRequestHeaders(),
        body: JSON.stringify({
            "": "",
        }),
    });
    if (response.ok === true) {
        var getData = ""; //RossAscends: reset to force array to update to account for deleted character.
        getData = await response.json();
        const load_ch_count = Object.getOwnPropertyNames(getData);
        for (var i = 0; i < load_ch_count.length; i++) {
            characters[i] = [];
            characters[i] = getData[i];
            characters[i]['name'] = DOMPurify.sanitize(characters[i]['name']);

            // For dropped-in cards
            if (!characters[i]['chat']) {
                characters[i]['chat'] = `${characters[i]['name']} - ${humanizedDateTime()}`;
            }

            characters[i]['chat'] = String(characters[i]['chat']);
        }
        if (this_chid != undefined && this_chid != "invalid-safety-id") {
            $("#avatar_url_pole").val(characters[this_chid].avatar);
        }

        await getGroups();
        await printCharacters(true);
    }
}

async function delChat(chatfile) {
    const response = await fetch("/delchat", {
        method: "POST",
        headers: getRequestHeaders(),
        body: JSON.stringify({
            chatfile: chatfile,
            avatar_url: characters[this_chid].avatar,
        }),
    });
    if (response.ok === true) {
        // choose another chat if current was deleted
        const name = chatfile.replace('.jsonl', '');
        if (name === characters[this_chid].chat) {
            chat_metadata = {};
            await replaceCurrentChat();
        }
        await eventSource.emit(event_types.CHAT_DELETED, name);
    }
}

async function replaceCurrentChat() {
    clearChat();
    chat.length = 0;

    const chatsResponse = await fetch("/getallchatsofcharacter", {
        method: 'POST',
        headers: getRequestHeaders(),
        body: JSON.stringify({ avatar_url: characters[this_chid].avatar })
    });

    if (chatsResponse.ok) {
        const chats = Object.values(await chatsResponse.json());

        // pick existing chat
        if (chats.length && typeof chats[0] === 'object') {
            characters[this_chid].chat = chats[0].file_name.replace('.jsonl', '');
            $("#selected_chat_pole").val(characters[this_chid].chat);
            saveCharacterDebounced();
            await getChat();
        }

        // start new chat
        else {
            characters[this_chid].chat = name2 + " - " + humanizedDateTime();
            $("#selected_chat_pole").val(characters[this_chid].chat);
            saveCharacterDebounced();
            await getChat();
        }
    }
}

const TRUNCATION_THRESHOLD = 100;

function showMoreMessages() {
    let messageId = Number($('#chat').children('.mes').first().attr('mesid'));
    let count = TRUNCATION_THRESHOLD;

    console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length);
    const prevHeight = $('#chat').prop('scrollHeight');

    while (messageId > 0 && count > 0) {
        count--;
        messageId--;
        addOneMessage(chat[messageId], { insertBefore: messageId + 1, scroll: false, forceId: messageId });
    }

    if (messageId == 0) {
        $('#show_more_messages').remove();
    }

    const newHeight = $('#chat').prop('scrollHeight');
    $('#chat').scrollTop(newHeight - prevHeight);
}

async function printMessages() {
    let startIndex = 0;

    if (chat.length > TRUNCATION_THRESHOLD) {
        count_view_mes = chat.length - TRUNCATION_THRESHOLD;
        startIndex = count_view_mes;
        $('#chat').append('<div id="show_more_messages">Show more messages</div>');
    }

    for (let i = startIndex; i < chat.length; i++) {
        const item = chat[i];
        addOneMessage(item, { scroll: i === chat.length - 1 });
    }
}

function clearChat() {
    count_view_mes = 0;
    extension_prompts = {};
    $("#chat").children().remove();
    if ($('.zoomed_avatar[forChar]').length) {
        console.debug('saw avatars to remove')
        $('.zoomed_avatar[forChar]').remove();
    } else { console.debug('saw no avatars') }
    itemizedPrompts = [];
}

async function deleteLastMessage() {
    count_view_mes--;
    chat.length = chat.length - 1;
    $('#chat').children('.mes').last().remove();
    await eventSource.emit(event_types.MESSAGE_DELETED, chat.length);
}

export async function reloadCurrentChat() {
    clearChat();
    chat.length = 0;

    if (selected_group) {
        await getGroupChat(selected_group);
    }
    else if (this_chid) {
        await getChat();
    }
    else {
        resetChatState();
        await printMessages();
    }

    await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId());
}

function messageFormatting(mes, ch_name, isSystem, isUser) {
    if (mes) {
        mesForShowdownParse = mes;
    }

    if (!mes) {
        mes = '';
    }

    // Force isSystem = false on comment messages so they get formatted properly
    if (ch_name === COMMENT_NAME_DEFAULT && isSystem && !isUser) {
        isSystem = false;
    }

    // Prompt bias replacement should be applied on the raw message
    if (!power_user.show_user_prompt_bias && ch_name && !isUser && !isSystem) {
        mes = mes.replaceAll(substituteParams(power_user.user_prompt_bias), "");
    }

    if (!isSystem) {
        let regexPlacement;
        if (isUser) {
            regexPlacement = regex_placement.USER_INPUT;
        } else if (ch_name !== name2) {
            regexPlacement = regex_placement.SLASH_COMMAND;
        } else {
            regexPlacement = regex_placement.AI_OUTPUT;
        }

        // Always override the character name
        mes = getRegexedString(mes, regexPlacement, {
            characterOverride: ch_name,
            isMarkdown: true
        });
    }

    if (power_user.auto_fix_generated_markdown) {
        mes = fixMarkdown(mes, true);
    }

    if (!isSystem && power_user.encode_tags) {
        mes = mes.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
    }

    if ((this_chid === undefined || this_chid === "invalid-safety-id") && !selected_group) {
        mes = mes
            .replace(/\*\*(.+?)\*\*/g, "<b>$1</b>")
            .replace(/\n/g, "<br/>");
    } else if (!isSystem) {
        mes = mes.replace(/```[\s\S]*?```|``[\s\S]*?``|`[\s\S]*?`|(\".+?\")|(\u201C.+?\u201D)/gm, function (match, p1, p2) {
            if (p1) {
                return '<q>"' + p1.replace(/\"/g, "") + '"</q>';
            } else if (p2) {
                return '<q>“' + p2.replace(/\u201C|\u201D/g, "") + '”</q>';
            } else {
                return match;
            }
        });

        mes = mes.replaceAll('\\begin{align*}', '$$');
        mes = mes.replaceAll('\\end{align*}', '$$');
        mes = converter.makeHtml(mes);
        mes = replaceBiasMarkup(mes);

        mes = mes.replace(/<code(.*)>[\s\S]*?<\/code>/g, function (match) {
            // Firefox creates extra newlines from <br>s in code blocks, so we replace them before converting newlines to <br>s.
            return match.replace(/\n/gm, '\u0000');
        })
        mes = mes.replace(/\n/g, "<br/>");
        mes = mes.replace(/\u0000/g, "\n"); // Restore converted newlines
        mes = mes.trim();

        mes = mes.replace(/<code(.*)>[\s\S]*?<\/code>/g, function (match) {
            return match.replace(/&amp;/g, '&');
        });
    }

    /*
    // Hides bias from empty messages send with slash commands
    if (isSystem) {
        mes = mes.replace(/\{\{[\s\S]*?\}\}/gm, "");
    }
    */

    if (!power_user.allow_name2_display && ch_name && !isUser && !isSystem) {
        mes = mes.replace(new RegExp(`(^|\n)${ch_name}:`, 'g'), "$1");
    }

    mes = DOMPurify.sanitize(mes);

    return mes;
}

/**
 * Inserts or replaces an SVG icon adjacent to the provided message's timestamp.
 *
 * If the `extra.api` is "openai" and `extra.model` contains the substring "claude",
 * the function fetches the "claude.svg". Otherwise, it fetches the SVG named after
 * the value in `extra.api`.
 *
 * @param {JQuery<HTMLElement>} mes - The message element containing the timestamp where the icon should be inserted or replaced.
 * @param {Object} extra - Contains the API and model details.
 * @param {string} extra.api - The name of the API, used to determine which SVG to fetch.
 * @param {string} extra.model - The model name, used to check for the substring "claude".
 */
function insertSVGIcon(mes, extra) {
    // Determine the SVG filename
    let modelName;

    // Claude on OpenRouter or Anthropic
    if (extra.api === "openai" && extra.model?.toLowerCase().includes("claude")) {
        modelName = "claude";
    }
    // OpenAI on OpenRouter
    else if (extra.api === "openai" && extra.model?.toLowerCase().includes("openai")) {
        modelName = "openai";
    }
    // OpenRouter website model or other models
    else if (extra.api === "openai" && (extra.model === null || extra.model?.toLowerCase().includes("/"))) {
        modelName = "openrouter";
    }
    // Everything else
    else {
        modelName = extra.api;
    }

    const image = new Image();
    // Add classes for styling and identification
    image.classList.add('icon-svg', 'timestamp-icon');
    image.src = `/img/${modelName}.svg`;
    image.title = `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`;

    image.onload = async function () {
        // Check if an SVG already exists adjacent to the timestamp
        let existingSVG = mes.find('.timestamp').next('.timestamp-icon');

        if (existingSVG.length) {
            // Replace existing SVG
            existingSVG.replaceWith(image);
        } else {
            // Append the new SVG if none exists
            mes.find('.timestamp').after(image);
        }

        await SVGInject(this);
    };
}


function getMessageFromTemplate({
    mesId,
    characterName,
    isUser,
    avatarImg,
    bias,
    isSystem,
    title,
    timerValue,
    timerTitle,
    bookmarkLink,
    forceAvatar,
    timestamp,
    tokenCount,
    extra,
} = {}) {
    const mes = $('#message_template .mes').clone();
    mes.attr({
        'mesid': mesId,
        'ch_name': characterName,
        'is_user': isUser,
        'is_system': !!isSystem,
        'bookmark_link': bookmarkLink,
        'force_avatar': !!forceAvatar,
        'timestamp': timestamp,
    });
    mes.find('.avatar img').attr('src', avatarImg);
    mes.find('.ch_name .name_text').text(characterName);
    mes.find('.mes_bias').html(bias);
    mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);
    mes.find('.mesIDDisplay').text(`#${mesId}`);
    tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
    title && mes.attr('title', title);
    timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);

    if (power_user.timestamp_model_icon && extra?.api) {
        insertSVGIcon(mes, extra);
    }

    return mes;
}

export function updateMessageBlock(messageId, message) {
    const messageElement = $(`#chat [mesid="${messageId}"]`);
    const text = message?.extra?.display_text ?? message.mes;
    messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user));
    addCopyToCodeBlocks(messageElement)
    appendImageToMessage(message, messageElement);
}

export function appendImageToMessage(mes, messageElement) {
    if (mes.extra?.image) {
        const image = messageElement.find('.mes_img');
        const text = messageElement.find('.mes_text');
        const isInline = !!mes.extra?.inline_image;
        image.attr('src', mes.extra?.image);
        image.attr('title', mes.extra?.title || mes.title || '');
        messageElement.find(".mes_img_container").addClass("img_extra");
        image.toggleClass("img_inline", isInline);
        text.toggleClass('displayNone', !isInline);
    }
}

export function addCopyToCodeBlocks(messageElement) {
    const codeBlocks = $(messageElement).find("pre code");
    for (let i = 0; i < codeBlocks.length; i++) {
        hljs.highlightElement(codeBlocks.get(i));
        if (navigator.clipboard !== undefined) {
            const copyButton = document.createElement('i');
            copyButton.classList.add('fa-solid', 'fa-copy', 'code-copy');
            copyButton.title = 'Copy code';
            codeBlocks.get(i).appendChild(copyButton);
            copyButton.addEventListener('pointerup', function (event) {
                navigator.clipboard.writeText(codeBlocks.get(i).innerText);
                toastr.info('Copied!', '', { timeOut: 2000 });
            });
        }
    }
}


function addOneMessage(mes, { type = "normal", insertAfter = null, scroll = true, insertBefore = null, forceId = null } = {}) {
    var messageText = mes["mes"];
    const momentDate = timestampToMoment(mes.send_date);
    const timestamp = momentDate.isValid() ? momentDate.format('LL LT') : '';

    if (mes?.extra?.display_text) {
        messageText = mes.extra.display_text;
    }

    // Forbidden black magic
    // This allows to use "continue" on user messages
    if (type === 'swipe' && mes.swipe_id === undefined) {
        mes.swipe_id = 0;
        mes.swipes = [mes.mes];
    }

    if (mes.name === name1) {
        var characterName = name1; //set to user's name by default
    } else { var characterName = mes.name }

    var avatarImg = getUserAvatar(user_avatar);
    const isSystem = mes.is_system;
    const title = mes.title;
    generatedPromptCache = "";

    //for non-user mesages
    if (!mes["is_user"]) {
        if (mes.force_avatar) {
            avatarImg = mes.force_avatar;
        } else if (this_chid === undefined || this_chid === "invalid-safety-id") {
            avatarImg = system_avatar;
        } else {
            if (characters[this_chid].avatar != "none") {
                avatarImg = getThumbnailUrl('avatar', characters[this_chid].avatar);
            } else {
                avatarImg = default_avatar;
            }
        }
        //old processing:
        //if messge is from sytem, use the name provided in the message JSONL to proceed,
        //if not system message, use name2 (char's name) to proceed
        //characterName = mes.is_system || mes.force_avatar ? mes.name : name2;
    } else if (mes["is_user"] && mes["force_avatar"]) {
        // Special case for persona images.
        avatarImg = mes["force_avatar"];
    }

    if (count_view_mes == 0) {
        messageText = substituteParams(messageText);
    }
    messageText = messageFormatting(
        messageText,
        characterName,
        isSystem,
        mes.is_user,
    );
    const bias = messageFormatting(mes.extra?.bias ?? "");
    let bookmarkLink = mes?.extra?.bookmark_link ?? '';
    // Verify bookmarked chat still exists
    // Cohee: Commented out for now. I'm worried of performance issues.
    /*if (bookmarkLink !== '') {
        let chat_names = selected_group
            ? getGroupChatNames(selected_group)
            : Object.values(getPastCharacterChats()).map(({ file_name }) => file_name);

        if (!chat_names.includes(bookmarkLink)) {
            bookmarkLink = ''
        }
    }*/
    let params = {
        mesId: forceId ?? count_view_mes,
        characterName: characterName,
        isUser: mes.is_user,
        avatarImg: avatarImg,
        bias: bias,
        isSystem: isSystem,
        title: title,
        bookmarkLink: bookmarkLink,
        forceAvatar: mes.force_avatar,
        timestamp: timestamp,
        extra: mes.extra,
        tokenCount: mes.extra?.token_count,
        ...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count),
    };

    const HTMLForEachMes = getMessageFromTemplate(params);

    if (type !== 'swipe') {
        if (!insertAfter && !insertBefore) {
            $("#chat").append(HTMLForEachMes);
        }
        else if (insertAfter) {
            const target = $("#chat").find(`.mes[mesid="${insertAfter}"]`);
            $(HTMLForEachMes).insertAfter(target);
            $(HTMLForEachMes).find('.swipe_left').css('display', 'none');
            $(HTMLForEachMes).find('.swipe_right').css('display', 'none');
        } else {
            const target = $("#chat").find(`.mes[mesid="${insertBefore}"]`);
            $(HTMLForEachMes).insertBefore(target);
            $(HTMLForEachMes).find('.swipe_left').css('display', 'none');
            $(HTMLForEachMes).find('.swipe_right').css('display', 'none');
        }
    }

    function getMessageId() {
        if (typeof forceId == 'number') {
            return forceId;
        }

        return type == 'swipe' ? count_view_mes - 1 : count_view_mes;
    }

    const newMessageId = getMessageId();
    const newMessage = $(`#chat [mesid="${newMessageId}"]`);
    const isSmallSys = mes?.extra?.isSmallSys;
    newMessage.data("isSystem", isSystem);

    if (isSystem) {
        // newMessage.find(".mes_edit").hide();
        newMessage.find(".mes_prompt").hide(); //don't need prompt button for sys
    }

    if (isSmallSys === true) {
        newMessage.addClass('smallSysMes');
    }

    // don't need prompt button for user
    if (params.isUser === true) {
        newMessage.find(".mes_prompt").hide();
        //console.log(`hiding prompt for user mesID ${params.mesId}`);
    }

    //shows or hides the Prompt display button
    let mesIdToFind = type == 'swipe' ? params.mesId - 1 : params.mesId;  //Number(newMessage.attr('mesId'));

    //if we have itemized messages, and the array isn't null..
    if (params.isUser === false && itemizedPrompts.length !== 0 && itemizedPrompts.length !== null) {
        // console.log('looking through itemized prompts...');
        //console.log(`mesIdToFind = ${mesIdToFind} from ${params.avatarImg}`);
        //console.log(`itemizedPrompts.length = ${itemizedPrompts.length}`)
        //console.log(itemizedPrompts);

        for (var i = 0; i < itemizedPrompts.length; i++) {
            //console.log(`itemized array item ${i} is MesID ${Number(itemizedPrompts[i].mesId)}, does it match ${Number(mesIdToFind)}?`);
            if (Number(itemizedPrompts[i].mesId) === Number(mesIdToFind)) {
                newMessage.find(".mes_prompt").show();
                //console.log(`showing button for mesID ${params.mesId} from ${params.characterName}`);
                break;

            } /*else {
                console.log(`no cache obj for mesID ${mesIdToFind}, hiding this prompt button`);
                newMessage.find(".mes_prompt").hide();
                console.log(itemizedPrompts);
            } */
        }
    } else {
        //console.log('itemizedprompt array empty null, or user, hiding this prompt buttons');
        //$(".mes_prompt").hide();
        newMessage.find(".mes_prompt").hide();
        //console.log(itemizedPrompts);
    }

    newMessage.find('.avatar img').on('error', function () {
        $(this).hide();
        $(this).parent().html(`<div class="missing-avatar fa-solid fa-user-slash"></div>`);
    });

    if (type === 'swipe') {
        const swipeMessage = $("#chat").find(`[mesid="${count_view_mes - 1}"]`);
        swipeMessage.find('.mes_text').html('');
        swipeMessage.find('.mes_text').append(messageText);
        appendImageToMessage(mes, swipeMessage);
        swipeMessage.attr('title', title);
        swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
        if (power_user.timestamp_model_icon && params.extra?.api) {
            insertSVGIcon(swipeMessage, params.extra);
        }

        if (mes.swipe_id == mes.swipes.length - 1) {
            swipeMessage.find('.mes_timer').text(params.timerValue);
            swipeMessage.find('.mes_timer').attr('title', params.timerTitle);
            swipeMessage.find('.tokenCounterDisplay').text(`${params.tokenCount}t`);
        } else {
            swipeMessage.find('.mes_timer').html('');
            swipeMessage.find('.tokenCounterDisplay').html('');
        }
    } else if (typeof forceId == 'number') {
        $("#chat").find(`[mesid="${forceId}"]`).find('.mes_text').append(messageText);
        appendImageToMessage(mes, newMessage);
        hideSwipeButtons();
        showSwipeButtons();
    } else {
        $("#chat").find(`[mesid="${count_view_mes}"]`).find('.mes_text').append(messageText);
        appendImageToMessage(mes, newMessage);
        hideSwipeButtons();
        count_view_mes++;
    }

    addCopyToCodeBlocks(newMessage);

    // Don't scroll if not inserting last
    if (!insertAfter && !insertBefore && scroll) {
        $('#chat .mes').last().addClass('last_mes');
        $('#chat .mes').eq(-2).removeClass('last_mes');

        hideSwipeButtons();
        showSwipeButtons();
        scrollChatToBottom();
    }
}

function getUserAvatar(avatarImg) {
    return `User Avatars/${avatarImg}`;
}

/**
 * Formats the title for the generation timer.
 * @param {Date} gen_started Date when generation was started
 * @param {Date} gen_finished Date when generation was finished
 * @param {number} tokenCount Number of tokens generated (0 if not available)
 * @returns {Object} Object containing the formatted timer value and title
 * @example
 * const { timerValue, timerTitle } = formatGenerationTimer(gen_started, gen_finished, tokenCount);
 * console.log(timerValue); // 1.2s
 * console.log(timerTitle); // Generation queued: 12:34:56 7 Jan 2021\nReply received: 12:34:57 7 Jan 2021\nTime to generate: 1.2 seconds\nToken rate: 5 t/s
 */
function formatGenerationTimer(gen_started, gen_finished, tokenCount) {
    if (!gen_started || !gen_finished) {
        return {};
    }

    const dateFormat = 'HH:mm:ss D MMM YYYY';
    const start = moment(gen_started);
    const finish = moment(gen_finished);
    const seconds = finish.diff(start, 'seconds', true);
    const timerValue = `${seconds.toFixed(1)}s`;
    const timerTitle = [
        `Generation queued: ${start.format(dateFormat)}`,
        `Reply received: ${finish.format(dateFormat)}`,
        `Time to generate: ${seconds} seconds`,
        tokenCount > 0 ? `Token rate: ${Number(tokenCount / seconds).toFixed(1)} t/s` : '',
    ].join('\n');

    return { timerValue, timerTitle };
}

function scrollChatToBottom() {
    if (power_user.auto_scroll_chat_to_bottom) {
        const chatElement = $("#chat");
        let position = chatElement[0].scrollHeight;

        if (power_user.waifuMode) {
            const lastMessage = chatElement.find('.mes').last();
            if (lastMessage.length) {
                const lastMessagePosition = lastMessage.position().top;
                position = chatElement.scrollTop() + lastMessagePosition;
            }
        }

        chatElement.scrollTop(position);
    }
}

/**
 * Substitutes {{macro}} parameters in a string.
 * @param {string} content - The string to substitute parameters in.
 * @param {*} _name1 - The name of the user. Uses global name1 if not provided.
 * @param {*} _name2 - The name of the character. Uses global name2 if not provided.
 * @param {*} _original - The original message for {{original}} substitution.
 * @param {*} _group - The group members list for {{group}} substitution.
 * @returns {string} The string with substituted parameters.
 */
function substituteParams(content, _name1, _name2, _original, _group) {
    _name1 = _name1 ?? name1;
    _name2 = _name2 ?? name2;
    _group = _group ?? name2;

    if (!content) {
        return '';
    }

    // Replace {{original}} with the original message
    // Note: only replace the first instance of {{original}}
    // This will hopefully prevent the abuse
    if (typeof _original === 'string') {
        content = content.replace(/{{original}}/i, _original);
    }
    content = content.replace(/{{input}}/gi, String($('#send_textarea').val()));
    content = content.replace(/{{user}}/gi, _name1);
    content = content.replace(/{{char}}/gi, _name2);
    content = content.replace(/{{charIfNotGroup}}/gi, _group);
    content = content.replace(/{{group}}/gi, _group);

    content = content.replace(/<USER>/gi, _name1);
    content = content.replace(/<BOT>/gi, _name2);
    content = content.replace(/<CHARIFNOTGROUP>/gi, _group);
    content = content.replace(/<GROUP>/gi, _group);

    content = content.replace(/\{\{\/\/([\s\S]*?)\}\}/gm, "");

    content = content.replace(/{{time}}/gi, moment().format('LT'));
    content = content.replace(/{{date}}/gi, moment().format('LL'));
    content = content.replace(/{{weekday}}/gi, moment().format('dddd'));
    content = content.replace(/{{isotime}}/gi, moment().format('HH:mm'));
    content = content.replace(/{{isodate}}/gi, moment().format('YYYY-MM-DD'));

    content = content.replace(/{{datetimeformat +([^}]*)}}/gi, (_, format) => {
        const formattedTime = moment().format(format);
        return formattedTime;
    });
    content = content.replace(/{{idle_duration}}/gi, () => getTimeSinceLastMessage());
    content = content.replace(/{{time_UTC([-+]\d+)}}/gi, (_, offset) => {
        const utcOffset = parseInt(offset, 10);
        const utcTime = moment().utc().utcOffset(utcOffset).format('LT');
        return utcTime;
    });
    content = randomReplace(content);
    content = diceRollReplace(content);
    content = bannedWordsReplace(content);
    return content;
}

/**
 * Replaces banned words in macros with an empty string.
 * Adds them to textgenerationwebui ban list.
 * @param {string} inText Text to replace banned words in
 * @returns {string} Text without the "banned" macro
 */
function bannedWordsReplace(inText) {
    if (!inText) {
        return '';
    }

    const banPattern = /{{banned "(.*)"}}/gi;

    if (main_api == 'textgenerationwebui') {
        const bans = inText.matchAll(banPattern);
        if (bans) {
            for (const banCase of bans) {
                console.log("Found banned words in macros: " + banCase[1]);
                textgenerationwebui_banned_in_macros.push(banCase[1]);
            }
        }
    }

    inText = inText.replaceAll(banPattern, "");
    return inText;
}

function getTimeSinceLastMessage() {
    const now = moment();

    if (Array.isArray(chat) && chat.length > 0) {
        let lastMessage;
        let takeNext = false;

        for (let i = chat.length - 1; i >= 0; i--) {
            const message = chat[i];

            if (message.is_system) {
                continue;
            }

            if (message.is_user && takeNext) {
                lastMessage = message;
                break;
            }

            takeNext = true;
        }

        if (lastMessage?.send_date) {
            const lastMessageDate = timestampToMoment(lastMessage.send_date);
            const duration = moment.duration(now.diff(lastMessageDate));
            return duration.humanize();
        }
    }

    return 'just now';
}

function randomReplace(input, emptyListPlaceholder = '') {
    const randomPattern = /{{random[ : ]([^}]+)}}/gi;

    return input.replace(randomPattern, (match, listString) => {
        const list = listString.split(',').map(item => item.trim()).filter(item => item.length > 0);

        if (list.length === 0) {
            return emptyListPlaceholder;
        }

        var rng = new Math.seedrandom('added entropy.', { entropy: true });
        const randomIndex = Math.floor(rng() * list.length);

        //const randomIndex = Math.floor(Math.random() * list.length);
        return list[randomIndex];
    });
}

function diceRollReplace(input, invalidRollPlaceholder = '') {
    const rollPattern = /{{roll[ : ]([^}]+)}}/gi;

    return input.replace(rollPattern, (match, matchValue) => {
        let formula = matchValue.trim();

        if (isDigitsOnly(formula)) {
            formula = `1d${formula}`;
        }

        const isValid = droll.validate(formula);

        if (!isValid) {
            console.debug(`Invalid roll formula: ${formula}`);
            return invalidRollPlaceholder;
        }

        const result = droll.roll(formula);
        return new String(result.total);
    });
}

function getStoppingStrings(isImpersonate) {
    const charString = `\n${name2}:`;
    const userString = `\n${name1}:`;
    const result = isImpersonate ? [charString] : [userString];

    result.push(userString);

    // Add other group members as the stopping strings
    if (selected_group) {
        const group = groups.find(x => x.id === selected_group);

        if (group && Array.isArray(group.members)) {
            const names = group.members
                .map(x => characters.find(y => y.avatar == x))
                .filter(x => x && x.name && x.name !== name2)
                .map(x => `\n${x.name}:`);
            result.push(...names);
        }
    }

    result.push(...getInstructStoppingSequences());

    if (power_user.custom_stopping_strings) {
        const customStoppingStrings = getCustomStoppingStrings();
        result.push(...customStoppingStrings);
    }

    if (power_user.single_line) {
        result.unshift('\n');
    }

    return result.filter(onlyUnique);
}

/**
 * Background generation based on the provided prompt.
 * @param {string} quiet_prompt Instruction prompt for the AI
 * @param {boolean} quietToLoud Whether the message should be sent in a foreground (loud) or background (quiet) mode
 * @param {boolean} skipWIAN whether to skip addition of World Info and Author's Note into the prompt
 * @returns
 */
export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN) {
    console.log('got into genQuietPrompt')
    const skipWIANvalue = skipWIAN
    return await new Promise(
        async function promptPromise(resolve, reject) {
            if (quietToLoud === true) {
                try {
                    await Generate('quiet', { resolve, reject, quiet_prompt, quietToLoud: true, skipWIAN: skipWIAN, force_name2: true, });
                }
                catch {
                    reject();
                }
            }
            else {
                try {
                    console.log('going to generate non-QuietToLoud')
                    await Generate('quiet', { resolve, reject, quiet_prompt, quietToLoud: false, skipWIAN: skipWIAN, force_name2: true, });
                }
                catch {
                    reject();
                }
            }
        });
}

function processCommands(message, type) {
    if (type == "regenerate" || type == "swipe" || type == 'quiet') {
        return null;
    }

    const result = executeSlashCommands(message);
    $("#send_textarea").val(result.newText).trigger('input');

    // interrupt generation if the input was nothing but a command
    if (message.length > 0 && result.newText.length === 0) {
        return true;
    }

    return result.interrupt;
}

function sendSystemMessage(type, text, extra = {}) {
    const systemMessage = system_messages[type];

    if (!systemMessage) {
        return;
    }

    const newMessage = { ...systemMessage, send_date: getMessageTimeStamp() };

    if (text) {
        newMessage.mes = text;
    }

    if (type == system_message_types.SLASH_COMMANDS) {
        newMessage.mes = getSlashCommandsHelp();
    }

    if (!newMessage.extra) {
        newMessage.extra = {};
    }

    newMessage.extra = Object.assign(newMessage.extra, extra);
    newMessage.extra.type = type;

    chat.push(newMessage);
    addOneMessage(newMessage);
    is_send_press = false;
}

export function extractMessageBias(message) {
    if (!message) {
        return null;
    }

    try {
        const biasHandlebars = Handlebars.create();
        const biasMatches = [];
        biasHandlebars.registerHelper('bias', function (text) {
            biasMatches.push(text);
            return '';
        });
        const template = biasHandlebars.compile(message);
        template({});

        if (biasMatches && biasMatches.length > 0) {
            return ` ${biasMatches.join(" ")}`;
        }

        return '';
    } catch {
        return '';
    }
}

/**
 * Removes impersonated group member lines from the group member messages.
 * Doesn't do anything if group reply trimming is disabled.
 * @param {string} getMessage Group message
 * @returns Cleaned-up group message
 */
function cleanGroupMessage(getMessage) {
    if (power_user.disable_group_trimming) {
        return getMessage;
    }

    const group = groups.find((x) => x.id == selected_group);

    if (group && Array.isArray(group.members) && group.members) {
        for (let member of group.members) {
            const character = characters.find(x => x.avatar == member);

            if (!character) {
                continue;
            }

            const name = character.name;

            // Skip current speaker.
            if (name === name2) {
                continue;
            }

            const regex = new RegExp(`(^|\n)${escapeRegex(name)}:`);
            const nameMatch = getMessage.match(regex);
            if (nameMatch) {
                getMessage = getMessage.substring(0, nameMatch.index);
            }
        }
    }
    return getMessage;
}

function addPersonaDescriptionExtensionPrompt() {
    if (!power_user.persona_description) {
        return;
    }

    const promptPositions = [persona_description_positions.BOTTOM_AN, persona_description_positions.TOP_AN];

    if (promptPositions.includes(power_user.persona_description_position) && shouldWIAddPrompt) {
        const originalAN = extension_prompts[NOTE_MODULE_NAME].value
        const ANWithDesc = power_user.persona_description_position === persona_description_positions.TOP_AN
            ? `${power_user.persona_description}\n${originalAN}`
            : `${originalAN}\n${power_user.persona_description}`;

        setExtensionPrompt(NOTE_MODULE_NAME, ANWithDesc, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth]);
    }
}

function getAllExtensionPrompts() {
    const value = Object
        .values(extension_prompts)
        .filter(x => x.value)
        .map(x => x.value.trim())
        .join('\n');

    return value.length ? substituteParams(value) : '';
}

// Wrapper to fetch extension prompts by module name
function getExtensionPromptByName(moduleName) {
    if (moduleName) {
        return substituteParams(extension_prompts[moduleName]?.value);
    } else {
        return;
    }
}

function getExtensionPrompt(position = 0, depth = undefined, separator = "\n") {
    let extension_prompt = Object.keys(extension_prompts)
        .sort()
        .map((x) => extension_prompts[x])
        .filter(x => x.position == position && x.value && (depth === undefined || x.depth == depth))
        .map(x => x.value.trim())
        .join(separator);
    if (extension_prompt.length && !extension_prompt.startsWith(separator)) {
        extension_prompt = separator + extension_prompt;
    }
    if (extension_prompt.length && !extension_prompt.endsWith(separator)) {
        extension_prompt = extension_prompt + separator;
    }
    if (extension_prompt.length) {
        extension_prompt = substituteParams(extension_prompt);
    }
    return extension_prompt;
}

export function baseChatReplace(value, name1, name2) {
    if (value !== undefined && value.length > 0) {
        value = substituteParams(value, name1, name2);

        if (power_user.collapse_newlines) {
            value = collapseNewlines(value);
        }

        value = value.replace(/\r/g, '');
    }
    return value;
}

function isStreamingEnabled() {
    const noStreamSources = [chat_completion_sources.SCALE, chat_completion_sources.AI21, chat_completion_sources.PALM];
    return ((main_api == 'openai' && oai_settings.stream_openai && !noStreamSources.includes(oai_settings.chat_completion_source))
        || (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming)
        || (main_api == 'novel' && nai_settings.streaming_novel)
        || (main_api == 'textgenerationwebui' && textgenerationwebui_settings.streaming));
}

function showStopButton() {
    $('#mes_stop').css({ 'display': 'flex' });
}

function hideStopButton() {
    $('#mes_stop').css({ 'display': 'none' });
}

class StreamingProcessor {
    showMessageButtons(messageId) {
        if (messageId == -1) {
            return;
        }

        showStopButton();
        $(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'none' });
    }

    hideMessageButtons(messageId) {
        if (messageId == -1) {
            return;
        }

        hideStopButton();
        $(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'flex' });
    }

    async onStartStreaming(text) {
        let messageId = -1;

        if (this.type == "impersonate") {
            $('#send_textarea').val('').trigger('input');
        }
        else {
            await saveReply(this.type, text);
            messageId = count_view_mes - 1;
            this.showMessageButtons(messageId);
        }

        hideSwipeButtons();
        scrollChatToBottom();
        return messageId;
    }

    removePrefix(text) {
        const name1Marker = `${name1}: `;
        const name2Marker = `${name2}: `;

        if (text) {
            if (text.startsWith(name1Marker)) {
                text = text.replace(name1Marker, '');
            }
            if (text.startsWith(name2Marker)) {
                text = text.replace(name2Marker, '');
            }
        }
        return text;
    }

    onProgressStreaming(messageId, text, isFinal) {
        const isImpersonate = this.type == "impersonate";
        const isContinue = this.type == "continue";
        text = this.removePrefix(text);
        let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal);

        // Predict unbalanced asterisks / quotes during streaming
        const charsToBalance = ['*', '"', '```'];
        for (const char of charsToBalance) {
            if (!isFinal && isOdd(countOccurrences(processedText, char))) {
                // Add character at the end to balance it
                const separator = char.length > 1 ? '\n' : '';
                processedText = processedText.trimEnd() + separator + char;
            }
        }

        if (isImpersonate) {
            $('#send_textarea').val(processedText).trigger('input');
        }
        else {
            let currentTime = new Date();
            // Don't waste time calculating token count for streaming
            let currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(processedText, 0) : 0;
            const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);
            chat[messageId]['mes'] = processedText;
            chat[messageId]['gen_started'] = this.timeStarted;
            chat[messageId]['gen_finished'] = currentTime;

            if (currentTokenCount) {
                if (!chat[messageId]['extra']) {
                    chat[messageId]['extra'] = {};
                }

                chat[messageId]['extra']['token_count'] = currentTokenCount;
                const tokenCounter = $(`#chat .mes[mesid="${messageId}"] .tokenCounterDisplay`);
                tokenCounter.text(`${currentTokenCount}t`);
            }

            if ((this.type == 'swipe' || this.type === 'continue') && Array.isArray(chat[messageId]['swipes'])) {
                chat[messageId]['swipes'][chat[messageId]['swipe_id']] = processedText;
                chat[messageId]['swipe_info'][chat[messageId]['swipe_id']] = { 'send_date': chat[messageId]['send_date'], 'gen_started': chat[messageId]['gen_started'], 'gen_finished': chat[messageId]['gen_finished'], 'extra': JSON.parse(JSON.stringify(chat[messageId]['extra'])) };
            }

            let formattedText = messageFormatting(
                processedText,
                chat[messageId].name,
                chat[messageId].is_system,
                chat[messageId].is_user,
            );
            const mesText = $(`#chat .mes[mesid="${messageId}"] .mes_text`);
            mesText.html(formattedText);
            $(`#chat .mes[mesid="${messageId}"] .mes_timer`).text(timePassed.timerValue).attr('title', timePassed.timerTitle);
            this.setFirstSwipe(messageId);
        }

        if (!scrollLock) {
            scrollChatToBottom();
        }
    }

    async onFinishStreaming(messageId, text) {
        this.hideMessageButtons(this.messageId);
        this.onProgressStreaming(messageId, text, true);
        addCopyToCodeBlocks($(`#chat .mes[mesid="${messageId}"]`));

        if (this.type !== 'impersonate') {
            await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId);
            await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId);
        } else {
            await eventSource.emit(event_types.IMPERSONATE_READY, text);
        }

        await saveChatConditional();
        activateSendButtons();
        showSwipeButtons();
        setGenerationProgress(0);
        generatedPromptCache = '';

        //console.log("Generated text size:", text.length, text)

        if (power_user.auto_swipe) {
            function containsBlacklistedWords(str, blacklist, threshold) {
                const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
                const matches = str.match(regex) || [];
                return matches.length >= threshold;
            }

            const generatedTextFiltered = (text) => {
                if (text) {
                    if (power_user.auto_swipe_minimum_length) {
                        if (text.length < power_user.auto_swipe_minimum_length && text.length !== 0) {
                            console.log("Generated text size too small")
                            return true
                        }
                    }
                    if (power_user.auto_swipe_blacklist_threshold) {
                        if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
                            console.log("Generated text has blacklisted words")
                            return true
                        }
                    }
                }
                return false
            }

            if (generatedTextFiltered(text)) {
                swipe_right()
                return
            }
        }
        playMessageSound();
    }

    onErrorStreaming() {
        this.hideMessageButtons(this.messageId);
        $("#send_textarea").removeAttr('disabled');
        is_send_press = false;
        activateSendButtons();
        setGenerationProgress(0);
        showSwipeButtons();
    }

    setFirstSwipe(messageId) {
        if (this.type !== 'swipe' && this.type !== 'impersonate') {
            if (Array.isArray(chat[messageId]['swipes']) && chat[messageId]['swipes'].length === 1 && chat[messageId]['swipe_id'] === 0) {
                chat[messageId]['swipes'][0] = chat[messageId]['mes'];
                chat[messageId]['swipe_info'][0] = { 'send_date': chat[messageId]['send_date'], 'gen_started': chat[messageId]['gen_started'], 'gen_finished': chat[messageId]['gen_finished'], 'extra': JSON.parse(JSON.stringify(chat[messageId]['extra'])) };
            }
        }
    }

    onStopStreaming() {
        this.onErrorStreaming();
    }

    *nullStreamingGeneration() {
        throw new Error('Generation function for streaming is not hooked up');
    }

    constructor(type, force_name2, timeStarted) {
        this.result = "";
        this.messageId = -1;
        this.type = type;
        this.force_name2 = force_name2;
        this.isStopped = false;
        this.isFinished = false;
        this.generator = this.nullStreamingGeneration;
        this.abortController = new AbortController();
        this.firstMessageText = '...';
        this.timeStarted = timeStarted;
    }

    async generate() {
        if (this.messageId == -1) {
            this.messageId = await this.onStartStreaming(this.firstMessageText);
            await delay(1); // delay for message to be rendered
            scrollLock = false;
        }

        try {
            for await (const text of this.generator()) {
                if (this.isStopped) {
                    this.onStopStreaming();
                    return;
                }

                this.result = text;
                this.onProgressStreaming(this.messageId, message_already_generated + text);
            }
        }
        catch (err) {
            console.error(err);
            this.onErrorStreaming();
            this.isStopped = true;
            return;
        }

        this.isFinished = true;
        return this.result;
    }
}

/**
 * Generates a message using the provided prompt.
 * @param {string} prompt Prompt to generate a message from
 * @param {string} api API to use. Main API is used if not specified.
 */
export async function generateRaw(prompt, api) {
    if (!api) {
        api = main_api;
    }

    const abortController = new AbortController();
    const isInstruct = power_user.instruct.enabled && main_api !== 'openai' && main_api !== 'novel';

    prompt = substituteParams(prompt);
    prompt = api == 'novel' ? adjustNovelInstructionPrompt(prompt) : prompt;
    prompt = isInstruct ? formatInstructModeChat(name1, prompt, false, true, '', name1, name2, false) : prompt;
    prompt = isInstruct ? (prompt + formatInstructModePrompt(name2, false, '', name1, name2)) : (prompt + '\n');

    let generateData = {};

    switch (api) {
        case 'kobold':
        case 'koboldhorde':
            if (preset_settings === 'gui') {
                generateData = { prompt: prompt, gui_settings: true, max_length: amount_gen, max_context_length: max_context, };
            } else {
                const isHorde = api === 'koboldhorde';
                const koboldSettings = koboldai_settings[koboldai_setting_names[preset_settings]];
                generateData = getKoboldGenerationData(prompt, koboldSettings, amount_gen, max_context, isHorde, 'quiet');
            }
            break;
        case 'novel':
            const novelSettings = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]];
            generateData = getNovelGenerationData(prompt, novelSettings, amount_gen, false, null);
            break;
        case 'textgenerationwebui':
            generateData = getTextGenGenerationData(prompt, amount_gen, false, null);
            break;
        case 'openai':
            generateData = [{ role: 'user', content: prompt.trim() }];
    }

    let data = {};

    if (api == 'koboldhorde') {
        data = await generateHorde(prompt, generateData, abortController.signal, false);
    } else if (api == 'openai') {
        data = await sendOpenAIRequest('quiet', generateData, abortController.signal);
    } else {
        const generateUrl = getGenerateUrl(api);
        const response = await fetch(generateUrl, {
            method: 'POST',
            headers: getRequestHeaders(),
            cache: 'no-cache',
            body: JSON.stringify(generateData),
            signal: abortController.signal,
        });

        if (!response.ok) {
            const error = await response.json();
            throw error;
        }

        data = await response.json();
    }

    if (data.error) {
        throw new Error(data.error);
    }

    const message = cleanUpMessage(extractMessageFromData(data), false, false, true);

    if (!message) {
        throw new Error('No message generated');
    }

    return message;
}

async function Generate(type, { automatic_trigger, force_name2, resolve, reject, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal } = {}, dryRun = false) {
    console.log('Generate entered');
    setGenerationProgress(0);
    generation_started = new Date();

    // Don't recreate abort controller if signal is passed
    if (!(abortController && signal)) {
        abortController = new AbortController();
    }

    // OpenAI doesn't need instruct mode. Use OAI main prompt instead.
    const isInstruct = power_user.instruct.enabled && main_api !== 'openai';
    const isImpersonate = type == "impersonate";

    message_already_generated = isImpersonate ? `${name1}: ` : `${name2}: `;

    const interruptedByCommand = processCommands($("#send_textarea").val(), type);

    if (interruptedByCommand) {
        $("#send_textarea").val('').trigger('input');
        is_send_press = false;
        return;
    }

    if (
        main_api == 'textgenerationwebui' &&
        textgenerationwebui_settings.streaming &&
        textgenerationwebui_settings.type === textgen_types.OOBA &&
        !textgenerationwebui_settings.streaming_url) {
        toastr.error('Streaming URL is not set. Look it up in the console window when starting TextGen Web UI');
        is_send_press = false;
        return;
    }

    if (main_api == 'kobold' && kai_settings.streaming_kobold && !kai_flags.can_use_streaming) {
        toastr.error('Streaming is enabled, but the version of Kobold used does not support token streaming.', undefined, { timeOut: 10000, preventDuplicates: true, });
        is_send_press = false;
        return;
    }

    if (isHordeGenerationNotAllowed()) {
        is_send_press = false;
        return;
    }

    // Hide swipes if not in a dry run.
    if (!dryRun) {
        hideSwipeButtons();
    }

    // Set empty promise resolution functions
    if (typeof resolve !== 'function') {
        resolve = () => { };
    }
    if (typeof reject !== 'function') {
        reject = () => { };
    }

    if (selected_group && !is_group_generating && !dryRun) {
        generateGroupWrapper(false, type, { resolve, reject, quiet_prompt, force_chid, signal: abortController.signal });
        return;
    } else if (selected_group && !is_group_generating && dryRun) {
        const characterIndexMap = new Map(characters.map((char, index) => [char.avatar, index]));
        const group = groups.find((x) => x.id === selected_group);

        const enabledMembers = group.members.reduce((acc, member) => {
            if (!group.disabled_members.includes(member) && !acc.includes(member)) {
                acc.push(member);
            }
            return acc;
        }, []);

        const memberIds = enabledMembers
            .map((member) => characterIndexMap.get(member))
            .filter((index) => index !== undefined && index !== null);

        if (memberIds.length > 0) {
            setCharacterId(memberIds[0]);
            setCharacterName('');
        } else {
            console.log('No enabled members found');
            is_send_press = false;
            return;
        }
    }

    //#########QUIET PROMPT STUFF##############
    //this function just gives special care to novel quiet instruction prompts
    if (quiet_prompt) {
        quiet_prompt = substituteParams(quiet_prompt);
        quiet_prompt = main_api == 'novel' && !quietToLoud ? adjustNovelInstructionPrompt(quiet_prompt) : quiet_prompt;
    }

    if (true === dryRun ||
        (online_status != 'no_connection' && this_chid != undefined && this_chid !== 'invalid-safety-id')) {
        let textareaText;
        if (type !== 'regenerate' && type !== "swipe" && type !== 'quiet' && !isImpersonate && !dryRun) {
            is_send_press = true;
            textareaText = $("#send_textarea").val();
            $("#send_textarea").val('').trigger('input');
        } else {
            textareaText = "";
            if (chat.length && chat[chat.length - 1]['is_user']) {
                //do nothing? why does this check exist?
            }
            else if (type !== 'quiet' && type !== "swipe" && !isImpersonate && !dryRun && chat.length) {
                chat.length = chat.length - 1;
                count_view_mes -= 1;
                $('#chat').children().last().hide(250, function () {
                    $(this).remove();
                });
                await eventSource.emit(event_types.MESSAGE_DELETED, chat.length);
            }
        }

        if (!type && !textareaText && power_user.continue_on_send && !selected_group && chat.length && !chat[chat.length - 1]['is_user'] && !chat[chat.length - 1]['is_system']) {
            type = 'continue';
        }

        const isContinue = type == 'continue';

        // Rewrite the generation timer to account for the time passed for all the continuations.
        if (isContinue && chat.length) {
            const prevFinished = chat[chat.length - 1]['gen_finished'];
            const prevStarted = chat[chat.length - 1]['gen_started'];

            if (prevFinished && prevStarted) {
                const timePassed = prevFinished - prevStarted;
                generation_started = new Date(Date.now() - timePassed);
                chat[chat.length - 1]['gen_started'] = generation_started;
            }
        }

        if (!dryRun) {
            deactivateSendButtons();
        }

        let { messageBias, promptBias, isUserPromptBias } = getBiasStrings(textareaText, type);

        //*********************************
        //PRE FORMATING STRING
        //*********************************

        //for normal messages sent from user..
        if (textareaText != "" && !automatic_trigger && type !== 'quiet') {
            // If user message contains no text other than bias - send as a system message
            if (messageBias && replaceBiasMarkup(textareaText).trim().length === 0) {
                sendSystemMessage(system_message_types.GENERIC, ' ', { bias: messageBias });
            }
            else {
                await sendMessageAsUser(textareaText, messageBias);
            }
        }
        else if (textareaText == "" && !automatic_trigger && type === undefined && main_api == 'openai' && oai_settings.send_if_empty.trim().length > 0) {
            // Use send_if_empty if set and the user message is empty. Only when sending messages normally
            await sendMessageAsUser(oai_settings.send_if_empty.trim(), messageBias);
        }

        ////////////////////////////////////
        const scenarioText = chat_metadata['scenario'] || characters[this_chid].scenario;
        let charDescription = baseChatReplace(characters[this_chid].description.trim(), name1, name2);
        let charPersonality = baseChatReplace(characters[this_chid].personality.trim(), name1, name2);
        let scenario = baseChatReplace(scenarioText.trim(), name1, name2);
        let mesExamples = baseChatReplace(characters[this_chid].mes_example.trim(), name1, name2);
        let systemPrompt = power_user.prefer_character_prompt ? baseChatReplace(characters[this_chid].data?.system_prompt?.trim(), name1, name2) : '';
        let jailbreakPrompt = power_user.prefer_character_jailbreak ? baseChatReplace(characters[this_chid].data?.post_history_instructions?.trim(), name1, name2) : '';
        let personaDescription = baseChatReplace(power_user.persona_description.trim(), name1, name2);

        if (isInstruct) {
            systemPrompt = power_user.prefer_character_prompt && systemPrompt ? systemPrompt : baseChatReplace(power_user.instruct.system_prompt, name1, name2);
            systemPrompt = formatInstructModeSystemPrompt(substituteParams(systemPrompt, name1, name2, power_user.instruct.system_prompt));
        }

        if (selected_group) {
            const groupCards = getGroupCharacterCards(selected_group, Number(this_chid));

            if (groupCards) {
                charDescription = groupCards.description;
                charPersonality = groupCards.personality;
                scenario = groupCards.scenario;
                mesExamples = groupCards.mesExample;
            }
        }

        // Depth prompt (character-specific A/N)
        removeDepthPrompts();
        const groupDepthPrompts = getGroupDepthPrompts(selected_group, Number(this_chid));

        if (selected_group && Array.isArray(groupDepthPrompts) && groupDepthPrompts.length > 0) {
            groupDepthPrompts.forEach((value, index) => {
                setExtensionPrompt('DEPTH_PROMPT_' + index, value.text, extension_prompt_types.IN_CHAT, value.depth);
            });
        } else {
            const depthPromptText = baseChatReplace(characters[this_chid].data?.extensions?.depth_prompt?.prompt?.trim(), name1, name2) || '';
            const depthPromptDepth = characters[this_chid].data?.extensions?.depth_prompt?.depth ?? depth_prompt_depth_default;
            setExtensionPrompt('DEPTH_PROMPT', depthPromptText, extension_prompt_types.IN_CHAT, depthPromptDepth);
        }

        // Parse example messages
        if (!mesExamples.startsWith('<START>')) {
            mesExamples = '<START>\n' + mesExamples.trim();
        }
        if (mesExamples.replace(/<START>/gi, '').trim().length === 0) {
            mesExamples = '';
        }
        if (mesExamples && isInstruct) {
            mesExamples = formatInstructModeExamples(mesExamples, name1, name2)
        }

        const exampleSeparator = power_user.context.example_separator ? `${substituteParams(power_user.context.example_separator)}\n` : '';
        const blockHeading = main_api === 'openai' ? '<START>\n' : exampleSeparator;
        let mesExamplesArray = mesExamples.split(/<START>/gi).slice(1).map(block => `${blockHeading}${block.trim()}\n`);

        if (power_user.strip_examples)
            mesExamplesArray = []

        // First message in fresh 1-on-1 chat reacts to user/character settings changes
        if (chat.length) {
            chat[0].mes = substituteParams(chat[0].mes);
        }

        // Collect messages with usable content
        let coreChat = chat.filter(x => !x.is_system);
        if (type === 'swipe') {
            coreChat.pop();
        }

        // Determine token limit
        let this_max_context = getMaxContextSize();

        if (!dryRun) {
            console.debug('Running extension interceptors');
            await runGenerationInterceptors(coreChat, this_max_context);
        } else {
            console.debug('Skipping extension interceptors for dry run');
        }

        console.log(`Core/all messages: ${coreChat.length}/${chat.length}`);

        // kingbri MARK: - Make sure the prompt bias isn't the same as the user bias
        if ((promptBias && !isUserPromptBias) || power_user.always_force_name2 || main_api == 'novel') {
            force_name2 = true;
        }

        if (isImpersonate) {
            force_name2 = false;
        }

        //////////////////////////////////

        let chat2 = [];
        let continue_mag = '';
        for (let i = coreChat.length - 1, j = 0; i >= 0; i--, j++) {
            // For OpenAI it's only used in WI
            if (main_api == 'openai' && (!world_info || world_info.length === 0)) {
                console.debug('No WI, skipping chat2 for OAI');
                break;
            }

            chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, false);

            if (j === 0 && isInstruct) {
                // Reformat with the first output sequence (if any)
                chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, force_output_sequence.FIRST);
            }

            // Do not suffix the message for continuation
            if (i === 0 && isContinue) {
                if (isInstruct) {
                    // Reformat with the last output sequence (if any)
                    chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, force_output_sequence.LAST);
                }

                chat2[i] = chat2[i].slice(0, chat2[i].lastIndexOf(coreChat[j].mes) + coreChat[j].mes.length);
                continue_mag = coreChat[j].mes;
            }
        }

        // Adjust token limit for Horde
        let adjustedParams;
        if (main_api == 'koboldhorde' && (horde_settings.auto_adjust_context_length || horde_settings.auto_adjust_response_length)) {
            try {
                adjustedParams = await adjustHordeGenerationParams(max_context, amount_gen);
            }
            catch {
                activateSendButtons();
                return;
            }
            if (horde_settings.auto_adjust_context_length) {
                this_max_context = (adjustedParams.maxContextLength - adjustedParams.maxLength);
            }
        }

        // Extension added strings
        // Set non-WI AN
        setFloatingPrompt();
        // Add WI to prompt (and also inject WI to AN value via hijack)

        let { worldInfoString, worldInfoBefore, worldInfoAfter, worldInfoDepth } = await getWorldInfoPrompt(chat2, this_max_context);

        if (skipWIAN !== true) {
            console.log('skipWIAN not active, adding WIAN')
            // Add all depth WI entries to prompt
            if (Array.isArray(worldInfoDepth)) {
                worldInfoDepth.forEach((e) => {
                    const joinedEntries = e.entries.join("\n");
                    setExtensionPrompt(`customDepthWI-${e.depth}`, joinedEntries, extension_prompt_types.IN_CHAT, e.depth)
                });
            }
        } else {
            console.log('skipping WIAN')
        }

        // Add persona description to prompt
        addPersonaDescriptionExtensionPrompt();
        // Call combined AN into Generate
        let allAnchors = getAllExtensionPrompts();
        const beforeScenarioAnchor = getExtensionPrompt(extension_prompt_types.BEFORE_PROMPT).trimStart();
        const afterScenarioAnchor = getExtensionPrompt(extension_prompt_types.IN_PROMPT);
        let zeroDepthAnchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, 0, ' ');

        const storyStringParams = {
            description: charDescription,
            personality: charPersonality,
            persona: personaDescription,
            scenario: scenario,
            system: isInstruct ? systemPrompt : '',
            char: name2,
            user: name1,
            wiBefore: worldInfoBefore,
            wiAfter: worldInfoAfter,
            loreBefore: worldInfoBefore,
            loreAfter: worldInfoAfter,
        };

        const storyString = renderStoryString(storyStringParams);

        if (main_api === 'openai') {
            message_already_generated = '';
            setOpenAIMessages(coreChat);
            setOpenAIMessageExamples(mesExamplesArray);
        }

        // hack for regeneration of the first message
        if (chat2.length == 0) {
            chat2.push('');
        }

        let examplesString = '';
        let chatString = '';
        let cyclePrompt = '';

        function getMessagesTokenCount() {
            const encodeString = [
                storyString,
                examplesString,
                chatString,
                allAnchors,
                quiet_prompt,
                cyclePrompt,
            ].join('').replace(/\r/gm, '');
            return getTokenCount(encodeString, power_user.token_padding);
        }

        // Force pinned examples into the context
        let pinExmString;
        if (power_user.pin_examples) {
            pinExmString = examplesString = mesExamplesArray.join('');
        }

        // Only add the chat in context if past the greeting message
        if (isContinue && (chat2.length > 1 || main_api === 'openai')) {
            cyclePrompt = chat2.shift();
        }

        // Collect enough messages to fill the context
        let arrMes = [];
        let tokenCount = getMessagesTokenCount();
        for (let item of chat2) {
            // not needed for OAI prompting
            if (main_api == 'openai') {
                break;
            }

            tokenCount += getTokenCount(item.replace(/\r/gm, ''))
            chatString = item + chatString;
            if (tokenCount < this_max_context) {
                arrMes[arrMes.length] = item;
            } else {
                break;
            }

            // Prevent UI thread lock on tokenization
            await delay(1);
        }

        if (main_api !== 'openai') {
            setInContextMessages(arrMes.length, type);
        }

        // Estimate how many unpinned example messages fit in the context
        tokenCount = getMessagesTokenCount();
        let count_exm_add = 0;
        if (!power_user.pin_examples) {
            for (let example of mesExamplesArray) {
                tokenCount += getTokenCount(example.replace(/\r/gm, ''))
                examplesString += example;
                if (tokenCount < this_max_context) {
                    count_exm_add++;
                } else {
                    break;
                }
                await delay(1);
            }
        }

        let mesSend = [];
        console.debug('calling runGenerate');

        if (!dryRun) {
            streamingProcessor = isStreamingEnabled() ? new StreamingProcessor(type, force_name2, generation_started) : false;
        }

        if (isContinue) {
            // Coping mechanism for OAI spacing
            if ((main_api === 'openai') && !cyclePrompt.endsWith(' ')) {
                cyclePrompt += ' ';
                continue_mag += ' ';
            }

            // Save reply does add cycle text to the prompt, so it's not needed here
            streamingProcessor && (streamingProcessor.firstMessageText = '');
            message_already_generated = continue_mag;
        }

        const originalType = type;
        runGenerate(cyclePrompt);

        async function runGenerate(cycleGenerationPrompt = '') {
            if (!dryRun) {
                is_send_press = true;
            }

            generatedPromptCache += cycleGenerationPrompt;
            if (generatedPromptCache.length == 0 || type === 'continue') {
                console.debug('generating prompt');
                chatString = "";
                arrMes = arrMes.reverse();
                arrMes.forEach(function (item, i, arr) {// For added anchors and others
                    // OAI doesn't need all of this
                    if (main_api === 'openai') {
                        return;
                    }

                    // Cohee: I'm not even sure what this is for anymore
                    if (i === arrMes.length - 1 && type !== 'continue') {
                        item = item.replace(/\n?$/, '');
                    }

                    mesSend[mesSend.length] = { message: item, extensionPrompts: [] };
                });
            }

            let mesExmString = '';

            function setPromptString() {
                if (main_api == 'openai') {
                    return;
                }

                console.debug('--setting Prompt string');
                mesExmString = pinExmString ?? mesExamplesArray.slice(0, count_exm_add).join('');

                if (mesSend.length) {
                    mesSend[mesSend.length - 1].message = modifyLastPromptLine(mesSend[mesSend.length - 1].message);
                }
            }

            function modifyLastPromptLine(lastMesString) {
                //#########QUIET PROMPT STUFF PT2##############

                // Add quiet generation prompt at depth 0
                if (quiet_prompt && quiet_prompt.length) {

                    // here name1 is forced for all quiet prompts..why?
                    const name = name1;
                    //checks if we are in instruct, if so, formats the chat as such, otherwise just adds the quiet prompt
                    const quietAppend = isInstruct ? formatInstructModeChat(name, quiet_prompt, false, true, '', name1, name2, false) : `\n${quiet_prompt}`;

                    //This begins to fix quietPrompts (particularly /sysgen) for instruct
                    //previously instruct input sequence was being appended to the last chat message w/o '\n'
                    //and no output sequence was added after the input's content.
                    //TODO: respect output_sequence vs last_output_sequence settings
                    //TODO: decide how to prompt this to clarify who is talking 'Narrator', 'System', etc.
                    if (isInstruct) {
                        lastMesString += '\n' + quietAppend; // + power_user.instruct.output_sequence + '\n';
                    } else {
                        lastMesString += quietAppend;
                    }


                    // Ross: bailing out early prevents quiet prompts from respecting other instruct prompt toggles
                    // for sysgen, SD, and summary this is desireable as it prevents the AI from responding as char..
                    // but for idle prompting, we want the flexibility of the other prompt toggles, and to respect them as per settings in the extension
                    // need a detection for what the quiet prompt is being asked for...

                    // Bail out early?
                    if (quietToLoud !== true) {
                        return lastMesString;
                    }
                }


                // Get instruct mode line
                if (isInstruct && !isContinue) {
                    const name = isImpersonate ? name1 : name2;
                    lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2);
                }

                // Get non-instruct impersonation line
                if (!isInstruct && isImpersonate && !isContinue) {
                    const name = name1;
                    if (!lastMesString.endsWith('\n')) {
                        lastMesString += '\n';
                    }
                    lastMesString += name + ':';
                }

                // Add character's name
                // Force name append on continue
                if (!isInstruct && force_name2) {
                    if (!lastMesString.endsWith('\n')) {
                        lastMesString += '\n';
                    }
                    lastMesString += `${name2}:`;
                }

                return lastMesString;
            }

            // Clean up the already generated prompt for seamless addition
            function cleanupPromptCache(promptCache) {
                // Remove the first occurrance of character's name
                if (promptCache.trimStart().startsWith(`${name2}:`)) {
                    promptCache = promptCache.replace(`${name2}:`, '').trimStart();
                }

                // Remove the first occurrance of prompt bias
                if (promptCache.trimStart().startsWith(promptBias)) {
                    promptCache = promptCache.replace(promptBias, '');
                }

                // Add a space if prompt cache doesn't start with one
                if (!/^\s/.test(promptCache) && !isInstruct) {
                    promptCache = ' ' + promptCache;
                }

                return promptCache;
            }

            function checkPromptSize() {
                console.debug('---checking Prompt size');
                setPromptString();
                const prompt = [
                    storyString,
                    mesExmString,
                    mesSend.join(''),
                    generatedPromptCache,
                    allAnchors,
                    quiet_prompt,
                ].join('').replace(/\r/gm, '');
                let thisPromptContextSize = getTokenCount(prompt, power_user.token_padding);

                if (thisPromptContextSize > this_max_context) {        //if the prepared prompt is larger than the max context size...
                    if (count_exm_add > 0) {                            // ..and we have example mesages..
                        count_exm_add--;                            // remove the example messages...
                        checkPromptSize();                            // and try agin...
                    } else if (mesSend.length > 0) {                    // if the chat history is longer than 0
                        mesSend.shift();                            // remove the first (oldest) chat entry..
                        checkPromptSize();                            // and check size again..
                    } else {
                        //end
                        console.debug(`---mesSend.length = ${mesSend.length}`);
                    }
                }
            }

            if (generatedPromptCache.length > 0 && main_api !== 'openai') {
                console.debug('---Generated Prompt Cache length: ' + generatedPromptCache.length);
                checkPromptSize();
            } else {
                console.debug('---calling setPromptString ' + generatedPromptCache.length)
                setPromptString();
            }

            // Fetches the combined prompt for both negative and positive prompts
            const cfgGuidanceScale = getGuidanceScale();

            // For prompt bit itemization
            let mesSendString = '';

            function getCombinedPrompt(isNegative) {
                // Only return if the guidance scale doesn't exist or the value is 1
                // Also don't return if constructing the neutral prompt
                if (isNegative && (!cfgGuidanceScale || cfgGuidanceScale?.value === 1)) {
                    return;
                }

                // OAI has its own prompt manager. No need to do anything here
                if (main_api === 'openai') {
                    return ''
                }

                // Deep clone
                let finalMesSend = structuredClone(mesSend);

                // TODO: Rewrite getExtensionPrompt to not require multiple for loops
                // Set all extension prompts where insertion depth > mesSend length
                if (finalMesSend.length) {
                    for (let upperDepth = MAX_INJECTION_DEPTH; upperDepth >= finalMesSend.length; upperDepth--) {
                        const upperAnchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, upperDepth);
                        if (upperAnchor && upperAnchor.length) {
                            finalMesSend[0].extensionPrompts.push(upperAnchor);
                        }
                    }
                }

                finalMesSend.forEach((mesItem, index) => {
                    if (index === 0) {
                        return;
                    }

                    const anchorDepth = Math.abs(index - finalMesSend.length);
                    // NOTE: Depth injected here!
                    const extensionAnchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, anchorDepth);

                    if (anchorDepth >= 0 && extensionAnchor && extensionAnchor.length) {
                        mesItem.extensionPrompts.push(extensionAnchor);
                    }
                });

                // TODO: Move zero-depth anchor append to work like CFG and bias appends
                if (zeroDepthAnchor?.length && !isContinue) {
                    console.debug(/\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1)))
                    finalMesSend[finalMesSend.length - 1].message +=
                        /\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1))
                            ? zeroDepthAnchor
                            : `${zeroDepthAnchor}`;
                }

                let cfgPrompt = {};
                if (cfgGuidanceScale && cfgGuidanceScale?.value !== 1) {
                    cfgPrompt = getCfgPrompt(cfgGuidanceScale, isNegative);
                }

                if (cfgPrompt && cfgPrompt?.value) {
                    if (cfgPrompt?.depth === 0) {
                        finalMesSend[finalMesSend.length - 1].message +=
                            /\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1))
                                ? cfgPrompt.value
                                : ` ${cfgPrompt.value}`;
                    } else {
                        // TODO: Make all extension prompts use an array/splice method
                        const lengthDiff = mesSend.length - cfgPrompt.depth;
                        const cfgDepth = lengthDiff >= 0 ? lengthDiff : 0;
                        finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`);
                    }
                }

                // Add prompt bias after everything else
                // Always run with continue
                if (!isInstruct && !isImpersonate) {
                    if (promptBias.trim().length !== 0) {
                        finalMesSend[finalMesSend.length - 1].message +=
                            /\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1))
                                ? promptBias.trimStart()
                                : ` ${promptBias.trimStart()}`;
                    }
                }

                // Prune from prompt cache if it exists
                if (generatedPromptCache.length !== 0) {
                    generatedPromptCache = cleanupPromptCache(generatedPromptCache);
                }

                // Right now, everything is suffixed with a newline
                mesSendString = finalMesSend.map((e) => `${e.extensionPrompts.join('')}${e.message}`).join('');

                // add chat preamble
                mesSendString = addChatsPreamble(mesSendString);

                // add a custom dingus (if defined)
                mesSendString = addChatsSeparator(mesSendString);

                let combinedPrompt =
                    beforeScenarioAnchor +
                    storyString +
                    afterScenarioAnchor +
                    mesExmString +
                    mesSendString +
                    generatedPromptCache;

                combinedPrompt = combinedPrompt.replace(/\r/gm, '');

                if (power_user.collapse_newlines) {
                    combinedPrompt = collapseNewlines(combinedPrompt);
                }

                return combinedPrompt;
            }

            // Get the negative prompt first since it has the unmodified mesSend array
            let negativePrompt = main_api == 'textgenerationwebui' ? getCombinedPrompt(true) : undefined;
            let finalPrompt = getCombinedPrompt(false);

            // Include the entire guidance scale object
            const cfgValues = cfgGuidanceScale && cfgGuidanceScale?.value !== 1 ? ({ guidanceScale: cfgGuidanceScale, negativePrompt: negativePrompt }) : null;

            let maxLength = Number(amount_gen); // how many tokens the AI will be requested to generate
            let thisPromptBits = [];

            // TODO: Make this a switch
            if (main_api == 'koboldhorde' && horde_settings.auto_adjust_response_length) {
                maxLength = Math.min(maxLength, adjustedParams.maxLength);
                maxLength = Math.max(maxLength, MIN_LENGTH); // prevent validation errors
            }

            let generate_data;
            if (main_api == 'koboldhorde' || main_api == 'kobold') {
                generate_data = {
                    prompt: finalPrompt,
                    gui_settings: true,
                    max_length: maxLength,
                    max_context_length: max_context,
                };

                if (preset_settings != 'gui') {
                    const isHorde = main_api == 'koboldhorde';
                    const presetSettings = koboldai_settings[koboldai_setting_names[preset_settings]];
                    const maxContext = (adjustedParams && horde_settings.auto_adjust_context_length) ? adjustedParams.maxContextLength : max_context;
                    generate_data = getKoboldGenerationData(finalPrompt, presetSettings, maxLength, maxContext, isHorde, type);
                }
            }
            else if (main_api == 'textgenerationwebui') {
                generate_data = getTextGenGenerationData(finalPrompt, maxLength, isImpersonate, cfgValues);
            }
            else if (main_api == 'novel') {
                const presetSettings = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]];
                generate_data = getNovelGenerationData(finalPrompt, presetSettings, maxLength, isImpersonate, cfgValues);
            }
            else if (main_api == 'openai') {
                let [prompt, counts] = prepareOpenAIMessages({
                    name2: name2,
                    charDescription: charDescription,
                    charPersonality: charPersonality,
                    Scenario: scenario,
                    worldInfoBefore: worldInfoBefore,
                    worldInfoAfter: worldInfoAfter,
                    extensionPrompts: extension_prompts,
                    bias: promptBias,
                    type: type,
                    quietPrompt: quiet_prompt,
                    cyclePrompt: cyclePrompt,
                    systemPromptOverride: systemPrompt,
                    jailbreakPromptOverride: jailbreakPrompt,
                    personaDescription: personaDescription
                }, dryRun);
                generate_data = { prompt: prompt };

                // counts will return false if the user has not enabled the token breakdown feature
                if (counts) {
                    parseTokenCounts(counts, thisPromptBits);
                }

                if (!dryRun) {
                    setInContextMessages(openai_messages_count, type);
                }
            }

            if (true === dryRun) return onSuccess({ error: 'dryRun' });

            if (power_user.console_log_prompts) {
                console.log(generate_data.prompt);
            }

            let generate_url = getGenerateUrl(main_api);
            console.debug('rungenerate calling API');

            showStopButton();

            //set array object for prompt token itemization of this message
            let currentArrayEntry = Number(thisPromptBits.length - 1);
            let additionalPromptStuff = {
                ...thisPromptBits[currentArrayEntry],
                rawPrompt: generate_data.prompt || generate_data.input,
                mesId: getNextMessageId(type),
                allAnchors: allAnchors,
                summarizeString: (extension_prompts['1_memory']?.value || ''),
                authorsNoteString: (extension_prompts['2_floating_prompt']?.value || ''),
                smartContextString: (extension_prompts['chromadb']?.value || ''),
                worldInfoString: worldInfoString,
                storyString: storyString,
                beforeScenarioAnchor: beforeScenarioAnchor,
                afterScenarioAnchor: afterScenarioAnchor,
                examplesString: examplesString,
                mesSendString: mesSendString,
                generatedPromptCache: generatedPromptCache,
                promptBias: promptBias,
                finalPrompt: finalPrompt,
                charDescription: charDescription,
                charPersonality: charPersonality,
                scenarioText: scenarioText,
                this_max_context: this_max_context,
                padding: power_user.token_padding,
                main_api: main_api,
                instruction: isInstruct ? substituteParams(power_user.prefer_character_prompt && systemPrompt ? systemPrompt : power_user.instruct.system_prompt) : '',
                userPersona: (power_user.persona_description || ''),
            };

            thisPromptBits = additionalPromptStuff;

            //console.log(thisPromptBits);

            itemizedPrompts.push(thisPromptBits);
            console.debug(`pushed prompt bits to itemizedPrompts array. Length is now: ${itemizedPrompts.length}`);

            if (main_api == 'openai') {
                if (isStreamingEnabled() && type !== 'quiet') {
                    streamingProcessor.generator = await sendOpenAIRequest(type, generate_data.prompt, streamingProcessor.abortController.signal);
                }
                else {
                    sendOpenAIRequest(type, generate_data.prompt, abortController.signal).then(onSuccess).catch(onError);
                }
            }
            else if (main_api == 'koboldhorde') {
                generateHorde(finalPrompt, generate_data, abortController.signal, true).then(onSuccess).catch(onError);
            }
            else if (main_api == 'textgenerationwebui' && isStreamingEnabled() && type !== 'quiet') {
                streamingProcessor.generator = await generateTextGenWithStreaming(generate_data, streamingProcessor.abortController.signal);
            }
            else if (main_api == 'novel' && isStreamingEnabled() && type !== 'quiet') {
                streamingProcessor.generator = await generateNovelWithStreaming(generate_data, streamingProcessor.abortController.signal);
            }
            else if (main_api == 'kobold' && isStreamingEnabled() && type !== 'quiet') {
                streamingProcessor.generator = await generateKoboldWithStreaming(generate_data, streamingProcessor.abortController.signal);
            }
            else {
                try {
                    const response = await fetch(generate_url, {
                        method: 'POST',
                        headers: getRequestHeaders(),
                        cache: 'no-cache',
                        body: JSON.stringify(generate_data),
                        signal: abortController.signal,
                    });

                    if (!response.ok) {
                        const error = await response.json();
                        throw error;
                    }

                    const data = await response.json();
                    onSuccess(data);
                } catch (error) {
                    onError(error);
                }
            }

            if (isStreamingEnabled() && type !== 'quiet') {
                hideSwipeButtons();
                let getMessage = await streamingProcessor.generate();
                let messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);

                if (isContinue) {
                    getMessage = continue_mag + getMessage;
                }

                if (streamingProcessor && !streamingProcessor.isStopped && streamingProcessor.isFinished) {
                    await streamingProcessor.onFinishStreaming(streamingProcessor.messageId, getMessage);
                    streamingProcessor = null;
                    triggerAutoContinue(messageChunk, isImpersonate);
                }
            }

            async function onSuccess(data) {
                let messageChunk = '';

                if (data.error == 'dryRun') {
                    generatedPromptCache = '';
                    resolve();
                    return;
                }

                if (!data.error) {
                    //const getData = await response.json();
                    let getMessage = extractMessageFromData(data);
                    let title = extractTitleFromData(data);
                    kobold_horde_model = title;

                    messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);

                    if (isContinue) {
                        getMessage = continue_mag + getMessage;
                    }

                    //Formating
                    const displayIncomplete = type == 'quiet';
                    getMessage = cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncomplete);

                    if (getMessage.length > 0) {
                        if (isImpersonate) {
                            $('#send_textarea').val(getMessage).trigger('input');
                            generatedPromptCache = "";
                            await eventSource.emit(event_types.IMPERSONATE_READY, getMessage);
                        }
                        else if (type == 'quiet') {
                            resolve(getMessage);
                        }
                        else {
                            // Without streaming we'll be having a full message on continuation. Treat it as a last chunk.
                            if (originalType !== 'continue') {
                                ({ type, getMessage } = await saveReply(type, getMessage, true, title));
                            }
                            else {
                                ({ type, getMessage } = await saveReply('appendFinal', getMessage, true, title));
                            }
                        }
                        activateSendButtons();

                        if (type !== 'quiet') {
                            playMessageSound();
                        }

                        generate_loop_counter = 0;
                    } else {
                        ++generate_loop_counter;

                        if (generate_loop_counter > MAX_GENERATION_LOOPS) {
                            throwCircuitBreakerError();
                        }

                        // regenerate with character speech reenforced
                        // to make sure we leave on swipe type while also adding the name2 appendage
                        setTimeout(() => {
                            Generate(type, { automatic_trigger, force_name2: true, resolve, reject, quiet_prompt, skipWIAN, force_chid });
                        }, generate_loop_counter * 1000);
                    }

                    if (power_user.auto_swipe) {
                        console.debug('checking for autoswipeblacklist on non-streaming message');
                        function containsBlacklistedWords(getMessage, blacklist, threshold) {
                            console.debug('checking blacklisted words');
                            const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
                            const matches = getMessage.match(regex) || [];
                            return matches.length >= threshold;
                        }

                        const generatedTextFiltered = (getMessage) => {
                            if (power_user.auto_swipe_blacklist_threshold) {
                                if (containsBlacklistedWords(getMessage, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
                                    console.debug("Generated text has blacklisted words")
                                    return true
                                }
                            }

                            return false
                        }
                        if (generatedTextFiltered(getMessage)) {
                            console.debug('swiping right automatically');
                            is_send_press = false;
                            swipe_right();
                            return
                        }
                    }
                } else {
                    generatedPromptCache = '';
                    activateSendButtons();
                    //console.log('runGenerate calling showSwipeBtns');
                    showSwipeButtons();

                    if (main_api == 'textgenerationwebui' && isMancer()) {
                        const errorText = `<h3>Inferencer endpoint is unhappy!</h3>
                        Returned status <tt>${data.status}</tt> with the reason:<br/>
                        ${data.response}`;
                        callPopup(errorText, 'text');
                    }
                }
                console.debug('/savechat called by /Generate');

                await saveChatConditional();
                is_send_press = false;
                hideStopButton();
                activateSendButtons();
                showSwipeButtons();
                setGenerationProgress(0);

                if (type !== 'quiet') {
                    triggerAutoContinue(messageChunk, isImpersonate);
                    resolve();
                }
            };

            function onError(exception) {
                if (typeof exception?.error?.message === 'string') {
                    toastr.error(exception.error.message, 'Error', { timeOut: 10000, extendedTimeOut: 20000 });
                }

                reject(exception);
                $("#send_textarea").removeAttr('disabled');
                is_send_press = false;
                activateSendButtons();
                showSwipeButtons();
                setGenerationProgress(0);
                console.log(exception);
            };

        } //rungenerate ends
    } else {    //generate's primary loop ends, after this is error handling for no-connection or safety-id
        if (this_chid === undefined || this_chid === 'invalid-safety-id') {
            toastr.warning('Сharacter is not selected');
        }
        is_send_press = false;
    }

    //prevent custom depth WI entries (which have unique random key names) from duplicating
    for (let key in extension_prompts) {
        if (key.includes('customDepthWI')) {
            let keyname = extension_prompts[key]
            delete extension_prompts[key];
        }
    }
    //console.log('generate ending');
} //generate ends

function getNextMessageId(type) {
    return type == 'swipe' ? Number(count_view_mes - 1) : Number(count_view_mes);
}

/**
 *
 * @param {string} messageChunk
 * @param {boolean} isImpersonate
 * @returns {void}
 */
export function triggerAutoContinue(messageChunk, isImpersonate) {
    if (selected_group) {
        console.log('Auto-continue is disabled for group chat');
        return;
    }

    if (power_user.auto_continue.enabled && !is_send_press) {
        if (power_user.auto_continue.target_length <= 0) {
            console.log('Auto-continue target length is 0, not triggering auto-continue');
            return;
        }

        if (main_api === 'openai' && !power_user.auto_continue.allow_chat_completions) {
            console.log('Auto-continue for OpenAI is disabled by user.');
            return;
        }

        if (isImpersonate) {
            console.log('Continue for impersonation is not implemented yet');
            return;
        }

        const textareaText = String($('#send_textarea').val());
        const USABLE_LENGTH = 5;

        if (textareaText.length > 0) {
            console.log('Not triggering auto-continue because user input is not empty');
            return;
        }

        if (messageChunk.trim().length > USABLE_LENGTH && chat.length) {
            const lastMessage = chat[chat.length - 1];
            const messageLength = getTokenCount(lastMessage.mes);
            const shouldAutoContinue = messageLength < power_user.auto_continue.target_length;

            if (shouldAutoContinue) {
                console.log(`Triggering auto-continue. Message tokens: ${messageLength}. Target tokens: ${power_user.auto_continue.target_length}. Message chunk: ${messageChunk}`);
                $("#option_continue").trigger('click');
            } else {
                console.log(`Not triggering auto-continue. Message tokens: ${messageLength}. Target tokens: ${power_user.auto_continue.target_length}`);
                return;
            }
        } else {
            console.log('Last generated chunk was empty, not triggering auto-continue');
            return;
        }
    }
}

export function getBiasStrings(textareaText, type) {
    if (type == 'impersonate' || type == 'continue') {
        return { messageBias: '', promptBias: '', isUserPromptBias: false };
    }

    let promptBias = '';
    let messageBias = extractMessageBias(textareaText);

    // If user input is not provided, retrieve the bias of the most recent relevant message
    if (!textareaText) {
        for (let i = chat.length - 1; i >= 0; i--) {
            const mes = chat[i];
            if (type === 'swipe' && chat.length - 1 === i) {
                continue;
            }
            if (mes && (mes.is_user || mes.is_system || mes.extra?.type === system_message_types.NARRATOR)) {
                if (mes.extra?.bias?.trim()?.length > 0) {
                    promptBias = mes.extra.bias;
                }
                break;
            }
        }
    }

    promptBias = messageBias || promptBias || power_user.user_prompt_bias || '';
    const isUserPromptBias = promptBias === power_user.user_prompt_bias;

    // Substitute params for everything
    messageBias = substituteParams(messageBias);
    promptBias = substituteParams(promptBias);

    return { messageBias, promptBias, isUserPromptBias };
}

/**
 * @param {Object} chatItem Message history item.
 * @param {boolean} isInstruct Whether instruct mode is enabled.
 * @param {boolean|number} forceOutputSequence Whether to force the first/last output sequence for instruct mode.
 */
function formatMessageHistoryItem(chatItem, isInstruct, forceOutputSequence) {
    const isNarratorType = chatItem?.extra?.type === system_message_types.NARRATOR;
    const characterName = chatItem?.name ? chatItem.name : name2;
    const itemName = chatItem.is_user ? chatItem['name'] : characterName;
    const shouldPrependName = !isNarratorType;

    let textResult = shouldPrependName ? `${itemName}: ${chatItem.mes}\n` : `${chatItem.mes}\n`;

    if (isInstruct) {
        textResult = formatInstructModeChat(itemName, chatItem.mes, chatItem.is_user, isNarratorType, chatItem.force_avatar, name1, name2, forceOutputSequence);
    }

    textResult = replaceBiasMarkup(textResult);

    return textResult;
}

export function replaceBiasMarkup(str) {
    return (str ?? '').replace(/\{\{[\s\S]*?\}\}/gm, '');
}

export async function sendMessageAsUser(textareaText, messageBias) {
    textareaText = getRegexedString(textareaText, regex_placement.USER_INPUT);

    chat[chat.length] = {};
    chat[chat.length - 1]['name'] = name1;
    chat[chat.length - 1]['is_user'] = true;
    chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
    chat[chat.length - 1]['mes'] = substituteParams(textareaText);
    chat[chat.length - 1]['extra'] = {};

    if (power_user.message_token_count_enabled) {
        chat[chat.length - 1]['extra']['token_count'] = getTokenCount(chat[chat.length - 1]['mes'], 0);
    }

    // Lock user avatar to a persona.
    if (user_avatar in power_user.personas) {
        chat[chat.length - 1]['force_avatar'] = getUserAvatar(user_avatar);
    }

    if (messageBias) {
        console.debug('checking bias');
        chat[chat.length - 1]['extra']['bias'] = messageBias;
    }
    statMesProcess(chat[chat.length - 1], 'user', characters, this_chid, '');
    // Wait for all handlers to finish before continuing with the prompt
    const chat_id = (chat.length - 1);
    await eventSource.emit(event_types.MESSAGE_SENT, chat_id);
    addOneMessage(chat[chat_id]);
    await eventSource.emit(event_types.USER_MESSAGE_RENDERED, chat_id);
    console.debug('message sent as user');
}

function getMaxContextSize() {
    let this_max_context = 1487;
    if (main_api == 'kobold' || main_api == 'koboldhorde' || main_api == 'textgenerationwebui') {
        this_max_context = (max_context - amount_gen);
    }
    if (main_api == 'novel') {
        this_max_context = Number(max_context);
        if (nai_settings.model_novel.includes('clio')) {
            this_max_context = Math.min(max_context, 8192);
        }
        if (nai_settings.model_novel.includes('kayra')) {
            this_max_context = Math.min(max_context, 8192);

            const subscriptionLimit = getKayraMaxContextTokens();
            if (typeof subscriptionLimit === "number" && this_max_context > subscriptionLimit) {
                this_max_context = subscriptionLimit;
                console.log(`NovelAI subscription limit reached. Max context size is now ${this_max_context}`);
            }
        }

        this_max_context = this_max_context - amount_gen;
    }
    if (main_api == 'openai') {
        this_max_context = oai_settings.openai_max_context;
    }
    return this_max_context;
}

function parseTokenCounts(counts, thisPromptBits) {
    /**
     * @param {any[]} numbers
     */
    function getSum(...numbers) {
        return numbers.map(x => Number(x)).filter(x => !Number.isNaN(x)).reduce((acc, val) => acc + val, 0);
    }
    const total = getSum(Object.values(counts));

    thisPromptBits.push({
        oaiStartTokens: (counts?.start + counts?.controlPrompts) || 0,
        oaiPromptTokens: getSum(counts?.prompt, counts?.charDescription, counts?.charPersonality, counts?.scenario) || 0,
        oaiBiasTokens: counts?.bias || 0,
        oaiNudgeTokens: counts?.nudge || 0,
        oaiJailbreakTokens: counts?.jailbreak || 0,
        oaiImpersonateTokens: counts?.impersonate || 0,
        oaiExamplesTokens: (counts?.dialogueExamples + counts?.examples) || 0,
        oaiConversationTokens: (counts?.conversation + counts?.chatHistory) || 0,
        oaiNsfwTokens: counts?.nsfw || 0,
        oaiMainTokens: counts?.main || 0,
        oaiTotalTokens: total,
    });
}

function addChatsPreamble(mesSendString) {
    return main_api === 'novel'
        ? substituteParams(nai_settings.preamble) + '\n' + mesSendString
        : mesSendString;
}

function addChatsSeparator(mesSendString) {
    if (power_user.context.chat_start) {
        return substituteParams(power_user.context.chat_start) + '\n' + mesSendString;
    }

    else {
        return mesSendString;
    }
}

function appendZeroDepthAnchor(force_name2, zeroDepthAnchor, finalPrompt) {
    const trimBothEnds = !force_name2;
    let trimmedPrompt = (trimBothEnds ? zeroDepthAnchor.trim() : zeroDepthAnchor.trimEnd());

    if (trimBothEnds && !finalPrompt.endsWith('\n')) {
        finalPrompt += '\n';
    }

    finalPrompt += trimmedPrompt;

    if (force_name2) {
        finalPrompt += ' ';
    }

    return finalPrompt;
}

async function DupeChar() {
    if (!this_chid) {
        toastr.warning('You must first select a character to duplicate!')
        return;
    }

    const confirm = await callPopup(`
        <h3>Are you sure you want to duplicate this character?</h3>
        <span>If you just want to start a new chat with the same character, use "Start new chat" option in the bottom-left options menu.</span><br><br>`,
        'confirm',
    );

    if (!confirm) {
        console.log('User cancelled duplication');
        return;
    }

    const body = { avatar_url: characters[this_chid].avatar };
    const response = await fetch('/dupecharacter', {
        method: 'POST',
        headers: getRequestHeaders(),
        body: JSON.stringify(body),
    });
    if (response.ok) {
        toastr.success("Character Duplicated");
        getCharacters();
    }
}

function promptItemize(itemizedPrompts, requestedMesId) {
    console.log('PROMPT ITEMIZE ENTERED');
    var incomingMesId = Number(requestedMesId);
    console.debug(`looking for MesId ${incomingMesId}`);
    var thisPromptSet = undefined;

    for (var i = 0; i < itemizedPrompts.length; i++) {
        console.log(`looking for ${incomingMesId} vs ${itemizedPrompts[i].mesId}`)
        if (itemizedPrompts[i].mesId === incomingMesId) {
            console.log(`found matching mesID ${i}`);
            thisPromptSet = i;
            PromptArrayItemForRawPromptDisplay = i;
            console.log(`wanting to raw display of ArrayItem: ${PromptArrayItemForRawPromptDisplay} which is mesID ${incomingMesId}`);
            console.log(itemizedPrompts[thisPromptSet]);
        }
    }

    if (thisPromptSet === undefined) {
        console.log(`couldnt find the right mesId. looked for ${incomingMesId}`);
        console.log(itemizedPrompts);
        return null;
    }

    //these happen regardless of API
    var charDescriptionTokens = getTokenCount(itemizedPrompts[thisPromptSet].charDescription);
    var charPersonalityTokens = getTokenCount(itemizedPrompts[thisPromptSet].charPersonality);
    var scenarioTextTokens = getTokenCount(itemizedPrompts[thisPromptSet].scenarioText);
    var userPersonaStringTokens = getTokenCount(itemizedPrompts[thisPromptSet].userPersona);
    var worldInfoStringTokens = getTokenCount(itemizedPrompts[thisPromptSet].worldInfoString);
    var allAnchorsTokens = getTokenCount(itemizedPrompts[thisPromptSet].allAnchors);
    var summarizeStringTokens = getTokenCount(itemizedPrompts[thisPromptSet].summarizeString);
    var authorsNoteStringTokens = getTokenCount(itemizedPrompts[thisPromptSet].authorsNoteString);
    var smartContextStringTokens = getTokenCount(itemizedPrompts[thisPromptSet].smartContextString);
    var beforeScenarioAnchorTokens = getTokenCount(itemizedPrompts[thisPromptSet].beforeScenarioAnchor);
    var afterScenarioAnchorTokens = getTokenCount(itemizedPrompts[thisPromptSet].afterScenarioAnchor);
    var zeroDepthAnchorTokens = getTokenCount(itemizedPrompts[thisPromptSet].zeroDepthAnchor);
    var thisPrompt_max_context = itemizedPrompts[thisPromptSet].this_max_context;
    var thisPrompt_padding = itemizedPrompts[thisPromptSet].padding;
    var this_main_api = itemizedPrompts[thisPromptSet].main_api;

    if (this_main_api == 'openai') {
        //for OAI API
        //console.log('-- Counting OAI Tokens');

        //var finalPromptTokens = itemizedPrompts[thisPromptSet].oaiTotalTokens;
        var oaiMainTokens = itemizedPrompts[thisPromptSet].oaiMainTokens;
        var oaiStartTokens = itemizedPrompts[thisPromptSet].oaiStartTokens;
        var ActualChatHistoryTokens = itemizedPrompts[thisPromptSet].oaiConversationTokens;
        var examplesStringTokens = itemizedPrompts[thisPromptSet].oaiExamplesTokens;
        var oaiPromptTokens = itemizedPrompts[thisPromptSet].oaiPromptTokens - (afterScenarioAnchorTokens + beforeScenarioAnchorTokens) + examplesStringTokens;
        var oaiBiasTokens = itemizedPrompts[thisPromptSet].oaiBiasTokens;
        var oaiJailbreakTokens = itemizedPrompts[thisPromptSet].oaiJailbreakTokens;
        var oaiNudgeTokens = itemizedPrompts[thisPromptSet].oaiNudgeTokens;
        var oaiImpersonateTokens = itemizedPrompts[thisPromptSet].oaiImpersonateTokens;
        var oaiNsfwTokens = itemizedPrompts[thisPromptSet].oaiNsfwTokens;
        var finalPromptTokens =
            oaiStartTokens +
            oaiPromptTokens +
            oaiMainTokens +
            oaiNsfwTokens +
            oaiBiasTokens +
            oaiImpersonateTokens +
            oaiJailbreakTokens +
            oaiNudgeTokens +
            ActualChatHistoryTokens +
            //charDescriptionTokens +
            //charPersonalityTokens +
            //allAnchorsTokens +
            worldInfoStringTokens +
            beforeScenarioAnchorTokens +
            afterScenarioAnchorTokens;
        // OAI doesn't use padding
        thisPrompt_padding = 0;
        // Max context size - max completion tokens
        thisPrompt_max_context = (oai_settings.openai_max_context - oai_settings.openai_max_tokens);
    } else {
        //for non-OAI APIs
        //console.log('-- Counting non-OAI Tokens');
        var finalPromptTokens = getTokenCount(itemizedPrompts[thisPromptSet].finalPrompt);
        var storyStringTokens = getTokenCount(itemizedPrompts[thisPromptSet].storyString) - worldInfoStringTokens;
        var examplesStringTokens = getTokenCount(itemizedPrompts[thisPromptSet].examplesString);
        var mesSendStringTokens = getTokenCount(itemizedPrompts[thisPromptSet].mesSendString)
        var ActualChatHistoryTokens = mesSendStringTokens - (allAnchorsTokens - (beforeScenarioAnchorTokens + afterScenarioAnchorTokens)) + power_user.token_padding;
        var instructionTokens = getTokenCount(itemizedPrompts[thisPromptSet].instruction);
        var promptBiasTokens = getTokenCount(itemizedPrompts[thisPromptSet].promptBias);

        var totalTokensInPrompt =
            storyStringTokens +     //chardefs total
            worldInfoStringTokens +
            examplesStringTokens + // example messages
            ActualChatHistoryTokens +  //chat history
            allAnchorsTokens +      // AN and/or legacy anchors
            //afterScenarioAnchorTokens +       //only counts if AN is set to 'after scenario'
            //zeroDepthAnchorTokens +           //same as above, even if AN not on 0 depth
            promptBiasTokens;       //{{}}
        //- thisPrompt_padding;  //not sure this way of calculating is correct, but the math results in same value as 'finalPrompt'
    }

    if (this_main_api == 'openai') {
        //console.log('-- applying % on OAI tokens');
        var oaiStartTokensPercentage = ((oaiStartTokens / (finalPromptTokens)) * 100).toFixed(2);
        var storyStringTokensPercentage = (((afterScenarioAnchorTokens + beforeScenarioAnchorTokens + oaiPromptTokens) / (finalPromptTokens)) * 100).toFixed(2);
        var ActualChatHistoryTokensPercentage = ((ActualChatHistoryTokens / (finalPromptTokens)) * 100).toFixed(2);
        var promptBiasTokensPercentage = ((oaiBiasTokens / (finalPromptTokens)) * 100).toFixed(2);
        var worldInfoStringTokensPercentage = ((worldInfoStringTokens / (finalPromptTokens)) * 100).toFixed(2);
        var allAnchorsTokensPercentage = ((allAnchorsTokens / (finalPromptTokens)) * 100).toFixed(2);
        var selectedTokenizer = `tiktoken (${getTokenizerModel()})`;
        var oaiSystemTokens = oaiImpersonateTokens + oaiJailbreakTokens + oaiNudgeTokens + oaiStartTokens + oaiNsfwTokens + oaiMainTokens;
        var oaiSystemTokensPercentage = ((oaiSystemTokens / (finalPromptTokens)) * 100).toFixed(2);

    } else {
        //console.log('-- applying % on non-OAI tokens');
        var storyStringTokensPercentage = ((storyStringTokens / (totalTokensInPrompt)) * 100).toFixed(2);
        var ActualChatHistoryTokensPercentage = ((ActualChatHistoryTokens / (totalTokensInPrompt)) * 100).toFixed(2);
        var promptBiasTokensPercentage = ((promptBiasTokens / (totalTokensInPrompt)) * 100).toFixed(2);
        var worldInfoStringTokensPercentage = ((worldInfoStringTokens / (totalTokensInPrompt)) * 100).toFixed(2);
        var allAnchorsTokensPercentage = ((allAnchorsTokens / (totalTokensInPrompt)) * 100).toFixed(2);
        var selectedTokenizer = $("#tokenizer").find(':selected').text();
    }

    const params = {
        selectedTokenizer,
        this_main_api,
        storyStringTokensPercentage,
        worldInfoStringTokensPercentage,
        ActualChatHistoryTokensPercentage,
        allAnchorsTokensPercentage,
        promptBiasTokensPercentage,
        storyStringTokens,
        charDescriptionTokens,
        charPersonalityTokens,
        scenarioTextTokens,
        examplesStringTokens,
        userPersonaStringTokens,
        instructionTokens,
        worldInfoStringTokens,
        ActualChatHistoryTokens,
        allAnchorsTokens,
        summarizeStringTokens,
        authorsNoteStringTokens,
        smartContextStringTokens,
        promptBiasTokens,
        totalTokensInPrompt,
        finalPromptTokens,
        thisPrompt_max_context,
        thisPrompt_padding,
        thisPrompt_actual: thisPrompt_max_context - thisPrompt_padding,
        oaiSystemTokensPercentage,
        oaiStartTokensPercentage,
        oaiSystemTokens,
        oaiStartTokens,
        oaiJailbreakTokens,
        oaiNudgeTokens,
        oaiImpersonateTokens,
        oaiPromptTokens,
        oaiBiasTokens,
        oaiNsfwTokens,
        oaiMainTokens,
    };

    if (this_main_api == 'openai') {
        callPopup(renderTemplate('itemizationChat', params), 'text');

    } else {
        callPopup(renderTemplate('itemizationText', params), 'text');
    }
}

function setInContextMessages(lastmsg, type) {
    $("#chat .mes").removeClass('lastInContext');

    if (type === 'swipe' || type === 'regenerate' || type === 'continue') {
        lastmsg++;
    }

    const lastMessageBlock = $('#chat .mes:not([is_system="true"])').eq(-lastmsg);
    lastMessageBlock.addClass('lastInContext');

    if (lastMessageBlock.length === 0) {
        const firstMessageId = getFirstDisplayedMessageId();
        $(`#chat .mes[mesid="${firstMessageId}"`).addClass('lastInContext');
    }
}

function getGenerateUrl(api) {
    let generate_url = '';
    if (api == 'kobold') {
        generate_url = '/generate';
    } else if (api == 'textgenerationwebui') {
        generate_url = '/generate_textgenerationwebui';
    } else if (api == 'novel') {
        generate_url = '/api/novelai/generate';
    }
    return generate_url;
}

function throwCircuitBreakerError() {
    callPopup(`Could not extract reply in ${MAX_GENERATION_LOOPS} attempts. Try generating again`, 'text');
    generate_loop_counter = 0;
    $("#send_textarea").removeAttr('disabled');
    is_send_press = false;
    activateSendButtons();
    setGenerationProgress(0);
    showSwipeButtons();
    throw new Error('Generate circuit breaker interruption');
}

function extractTitleFromData(data) {
    if (main_api == 'koboldhorde') {
        return data.workerName;
    }

    return undefined;
}

function extractMessageFromData(data) {
    switch (main_api) {
        case 'kobold':
            return data.results[0].text;
        case 'koboldhorde':
            return data.text;
        case 'textgenerationwebui':
            return data.results[0].text;
        case 'novel':
            return data.output;
        case 'openai':
            return data;
        default:
            return ''
    }
}

function cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncompleteSentences = false) {
    // Add the prompt bias before anything else
    if (
        power_user.user_prompt_bias &&
        !isImpersonate &&
        !isContinue &&
        power_user.user_prompt_bias.length !== 0
    ) {
        getMessage = substituteParams(power_user.user_prompt_bias) + getMessage;
    }

    // Regex uses vars, so add before formatting
    getMessage = getRegexedString(getMessage, isImpersonate ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT);

    if (!displayIncompleteSentences && power_user.trim_sentences) {
        getMessage = trimToEndSentence(getMessage, power_user.include_newline);
    }

    if (power_user.collapse_newlines) {
        getMessage = collapseNewlines(getMessage);
    }

    if (power_user.trim_spaces) {
        getMessage = getMessage.trim();
    }
    // trailing invisible whitespace before every newlines, on a multiline string
    // "trailing whitespace on newlines       \nevery line of the string    \n?sample text" ->
    // "trailing whitespace on newlines\nevery line of the string\nsample text"
    getMessage = getMessage.replace(/[^\S\r\n]+$/gm, "");

    let nameToTrim = isImpersonate ? name2 : name1;

    if (isImpersonate) {
        nameToTrim = power_user.allow_name2_display ? '' : name2;
    }
    else {
        nameToTrim = power_user.allow_name1_display ? '' : name1;
    }

    if (nameToTrim && getMessage.indexOf(`${nameToTrim}:`) == 0) {
        getMessage = getMessage.substr(0, getMessage.indexOf(`${nameToTrim}:`));
    }
    if (nameToTrim && getMessage.indexOf(`\n${nameToTrim}:`) >= 0) {
        getMessage = getMessage.substr(0, getMessage.indexOf(`\n${nameToTrim}:`));
    }
    if (getMessage.indexOf('<|endoftext|>') != -1) {
        getMessage = getMessage.substr(0, getMessage.indexOf('<|endoftext|>'));
    }
    const isInstruct = power_user.instruct.enabled && main_api !== 'openai';
    if (isInstruct && power_user.instruct.stop_sequence) {
        if (getMessage.indexOf(power_user.instruct.stop_sequence) != -1) {
            getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.stop_sequence));
        }
    }
    // Hana: Only use the first sequence (should be <|model|>)
    // of the prompt before <|user|> (as KoboldAI Lite does it).
    if (isInstruct && power_user.instruct.input_sequence) {
        if (getMessage.indexOf(power_user.instruct.input_sequence) != -1) {
            getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.input_sequence));
        }
    }
    if (isInstruct && power_user.instruct.input_sequence && isImpersonate) {
        //getMessage = getMessage.replaceAll(power_user.instruct.input_sequence, '');
        power_user.instruct.input_sequence.split('\n')
            .filter(line => line.trim() !== '')
            .forEach(line => {
                getMessage = getMessage.replaceAll(line, '');
            });
    }
    if (isInstruct && power_user.instruct.output_sequence && !isImpersonate) {
        //getMessage = getMessage.replaceAll(power_user.instruct.output_sequence, '');
        power_user.instruct.output_sequence.split('\n')
            .filter(line => line.trim() !== '')
            .forEach(line => {
                getMessage = getMessage.replaceAll(line, '');
            });
    }
    if (isInstruct && power_user.instruct.last_output_sequence && !isImpersonate) {
        //getMessage = getMessage.replaceAll(power_user.instruct.last_output_sequence, '');
        power_user.instruct.last_output_sequence.split('\n')
            .filter(line => line.trim() !== '')
            .forEach(line => {
                getMessage = getMessage.replaceAll(line, '');
            });
    }
    // clean-up group message from excessive generations
    if (selected_group) {
        getMessage = cleanGroupMessage(getMessage);
    }

    if (!power_user.allow_name2_display) {
        getMessage = getMessage.replace(new RegExp(`(^|\n)${name2}:`, 'g'), "$1");
    }

    if (isImpersonate) {
        getMessage = getMessage.trim();
    }

    const stoppingStrings = getStoppingStrings(isImpersonate);

    for (const stoppingString of stoppingStrings) {
        if (stoppingString.length) {
            for (let j = stoppingString.length - 1; j > 0; j--) {
                if (getMessage.slice(-j) === stoppingString.slice(0, j)) {
                    getMessage = getMessage.slice(0, -j);
                    break;
                }
            }
        }
    }
    if (power_user.auto_fix_generated_markdown) {
        getMessage = fixMarkdown(getMessage, false);
    }

    const nameToTrim2 = isImpersonate ? name1 : name2;

    if (getMessage.startsWith(nameToTrim2 + ":")) {
        getMessage = getMessage.replace(nameToTrim2 + ':', '');
        getMessage = getMessage.trimStart();
    }

    if (isImpersonate) {
        getMessage = getMessage.trim();
    }

    return getMessage;
}

async function saveReply(type, getMessage, _, title) {
    if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined ||
        chat[chat.length - 1]['is_user'])) {
        type = 'normal';
    }

    if (chat.length && typeof chat[chat.length - 1]['extra'] !== 'object') {
        chat[chat.length - 1]['extra'] = {};
    }

    let oldMessage = ''
    const generationFinished = new Date();
    const img = extractImageFromMessage(getMessage);
    getMessage = img.getMessage;
    if (type === 'swipe') {
        oldMessage = chat[chat.length - 1]['mes'];
        chat[chat.length - 1]['swipes'].length++;
        if (chat[chat.length - 1]['swipe_id'] === chat[chat.length - 1]['swipes'].length - 1) {
            chat[chat.length - 1]['title'] = title;
            chat[chat.length - 1]['mes'] = getMessage;
            chat[chat.length - 1]['gen_started'] = generation_started;
            chat[chat.length - 1]['gen_finished'] = generationFinished;
            chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
            chat[chat.length - 1]['extra']['api'] = getGeneratingApi();
            chat[chat.length - 1]['extra']['model'] = getGeneratingModel();
            if (power_user.message_token_count_enabled) {
                chat[chat.length - 1]['extra']['token_count'] = getTokenCount(chat[chat.length - 1]['mes'], 0);
            }
            const chat_id = (chat.length - 1);
            await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
            addOneMessage(chat[chat_id], { type: 'swipe' });
            await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id);
        } else {
            chat[chat.length - 1]['mes'] = getMessage;
        }
    } else if (type === 'append' || type === 'continue') {
        console.debug("Trying to append.")
        oldMessage = chat[chat.length - 1]['mes'];
        chat[chat.length - 1]['title'] = title;
        chat[chat.length - 1]['mes'] += getMessage;
        chat[chat.length - 1]['gen_started'] = generation_started;
        chat[chat.length - 1]['gen_finished'] = generationFinished;
        chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
        chat[chat.length - 1]["extra"]["api"] = getGeneratingApi();
        chat[chat.length - 1]["extra"]["model"] = getGeneratingModel();
        if (power_user.message_token_count_enabled) {
            chat[chat.length - 1]['extra']['token_count'] = getTokenCount(chat[chat.length - 1]['mes'], 0);
        }
        const chat_id = (chat.length - 1);
        await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
        addOneMessage(chat[chat_id], { type: 'swipe' });
        await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id);
    } else if (type === 'appendFinal') {
        oldMessage = chat[chat.length - 1]['mes'];
        console.debug("Trying to appendFinal.")
        chat[chat.length - 1]['title'] = title;
        chat[chat.length - 1]['mes'] = getMessage;
        chat[chat.length - 1]['gen_started'] = generation_started;
        chat[chat.length - 1]['gen_finished'] = generationFinished;
        chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
        chat[chat.length - 1]["extra"]["api"] = getGeneratingApi();
        chat[chat.length - 1]["extra"]["model"] = getGeneratingModel();
        if (power_user.message_token_count_enabled) {
            chat[chat.length - 1]['extra']['token_count'] = getTokenCount(chat[chat.length - 1]['mes'], 0);
        }
        const chat_id = (chat.length - 1);
        await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
        addOneMessage(chat[chat_id], { type: 'swipe' });
        await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id);

    } else {
        console.debug('entering chat update routine for non-swipe post');
        chat[chat.length] = {};
        chat[chat.length - 1]['extra'] = {};
        chat[chat.length - 1]['name'] = name2;
        chat[chat.length - 1]['is_user'] = false;
        chat[chat.length - 1]['send_date'] = getMessageTimeStamp();
        chat[chat.length - 1]["extra"]["api"] = getGeneratingApi();
        chat[chat.length - 1]["extra"]["model"] = getGeneratingModel();
        if (power_user.trim_spaces) {
            getMessage = getMessage.trim();
        }
        chat[chat.length - 1]['mes'] = getMessage;
        chat[chat.length - 1]['title'] = title;
        chat[chat.length - 1]['gen_started'] = generation_started;
        chat[chat.length - 1]['gen_finished'] = generationFinished;

        if (power_user.message_token_count_enabled) {
            chat[chat.length - 1]['extra']['token_count'] = getTokenCount(chat[chat.length - 1]['mes'], 0);
        }

        if (selected_group) {
            console.debug('entering chat update for groups');
            let avatarImg = 'img/ai4.png';
            if (characters[this_chid].avatar != 'none') {
                avatarImg = getThumbnailUrl('avatar', characters[this_chid].avatar);
            }
            chat[chat.length - 1]['force_avatar'] = avatarImg;
            chat[chat.length - 1]['original_avatar'] = characters[this_chid].avatar;
            chat[chat.length - 1]['extra']['gen_id'] = group_generation_id;
        }

        saveImageToMessage(img, chat[chat.length - 1]);
        const chat_id = (chat.length - 1);
        await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
        addOneMessage(chat[chat_id]);
        await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id);
    }

    const item = chat[chat.length - 1];
    if (item["swipe_info"] === undefined) {
        item["swipe_info"] = [];
    }
    if (item["swipe_id"] !== undefined) {
        const swipeId = item["swipe_id"];
        item["swipes"][swipeId] = item["mes"];
        item["swipe_info"][swipeId] = {
            send_date: item["send_date"],
            gen_started: item["gen_started"],
            gen_finished: item["gen_finished"],
            extra: JSON.parse(JSON.stringify(item["extra"])),
        };
    } else {
        item["swipe_id"] = 0;
        item["swipes"] = [];
        item["swipes"][0] = chat[chat.length - 1]["mes"];
        item["swipe_info"][0] = {
            send_date: chat[chat.length - 1]["send_date"],
            gen_started: chat[chat.length - 1]["gen_started"],
            gen_finished: chat[chat.length - 1]["gen_finished"],
            extra: JSON.parse(JSON.stringify(chat[chat.length - 1]["extra"])),
        };
    }
    statMesProcess(chat[chat.length - 1], type, characters, this_chid, oldMessage);
    return { type, getMessage };
}

function saveImageToMessage(img, mes) {
    if (mes && img.image) {
        if (typeof mes.extra !== 'object') {
            mes.extra = {};
        }
        mes.extra.image = img.image;
        mes.extra.title = img.title;
    }
}

function getGeneratingApi() {
    switch (main_api) {
        case 'openai':
            return oai_settings.chat_completion_source || 'openai';
        default:
            return main_api;
    }
}

function getGeneratingModel(mes) {
    let model = '';
    switch (main_api) {
        case 'kobold':
            model = online_status;
            break;
        case 'novel':
            model = nai_settings.model_novel;
            break;
        case 'openai':
            model = getChatCompletionModel();
            break;
        case 'textgenerationwebui':
            model = online_status;
            break;
        case 'koboldhorde':
            model = kobold_horde_model;
            break;
    }
    return model
}

function extractImageFromMessage(getMessage) {
    const regex = /<img src="(.*?)".*?alt="(.*?)".*?>/g;
    const results = regex.exec(getMessage);
    const image = results ? results[1] : '';
    const title = results ? results[2] : '';
    getMessage = getMessage.replace(regex, '');
    return { getMessage, image, title };
}

export function activateSendButtons() {
    is_send_press = false;
    $("#send_but").removeClass("displayNone");
    $("#mes_continue").removeClass("displayNone");
    $("#send_textarea").attr("disabled", false);
    $('.mes_buttons:last').show();
    hideStopButton();
}

export function deactivateSendButtons() {
    $("#send_but").addClass("displayNone");
    $("#mes_continue").addClass("displayNone");
    showStopButton();
}

function resetChatState() {
    //unsets expected chid before reloading (related to getCharacters/printCharacters from using old arrays)
    this_chid = "invalid-safety-id";
    // replaces deleted charcter name with system user since it will be displayed next.
    name2 = systemUserName;
    // sets up system user to tell user about having deleted a character
    chat = [...safetychat];
    // resets chat metadata
    chat_metadata = {};
    // resets the characters array, forcing getcharacters to reset
    characters.length = 0;
}

export function setMenuType(value) {
    menu_type = value;
}

export function setExternalAbortController(controller) {
    abortController = controller;
}

function setCharacterId(value) {
    this_chid = value;
}

function setCharacterName(value) {
    name2 = value;
}

function setOnlineStatus(value) {
    online_status = value;
}

function setEditedMessageId(value) {
    this_edit_mes_id = value;
}

function setSendButtonState(value) {
    is_send_press = value;
}

function resultCheckStatusNovel() {
    is_api_button_press_novel = false;
    checkOnlineStatus();
    $("#api_loading_novel").css("display", "none");
    $("#api_button_novel").css("display", "inline-block");
}

async function renameCharacter() {
    const oldAvatar = characters[this_chid].avatar;
    const newValue = await callPopup('<h3>New name:</h3>', 'input', characters[this_chid].name);

    if (newValue && newValue !== characters[this_chid].name) {
        const body = JSON.stringify({ avatar_url: oldAvatar, new_name: newValue });
        const response = await fetch('/renamecharacter', {
            method: 'POST',
            headers: getRequestHeaders(),
            body,
        });

        try {
            if (response.ok) {
                const data = await response.json();
                const newAvatar = data.avatar;

                // Replace tags list
                renameTagKey(oldAvatar, newAvatar);

                // Reload characters list
                await getCharacters();

                // Find newly renamed character
                const newChId = characters.findIndex(c => c.avatar == data.avatar);

                if (newChId !== -1) {
                    // Select the character after the renaming
                    this_chid = -1;
                    selectCharacterById(String(newChId));

                    // Async delay to update UI
                    await delay(1);

                    if (this_chid === -1) {
                        throw new Error('New character not selected');
                    }

                    // Also rename as a group member
                    await renameGroupMember(oldAvatar, newAvatar, newValue);
                    const renamePastChatsConfirm = await callPopup(`<h3>Character renamed!</h3>
                    <p>Past chats will still contain the old character name. Would you like to update the character name in previous chats as well?</p>
                    <i><b>Sprites folder (if any) should be renamed manually.</b></i>`, 'confirm');

                    if (renamePastChatsConfirm) {
                        await renamePastChats(newAvatar, newValue);
                        await reloadCurrentChat();
                        toastr.success('Character renamed and past chats updated!');
                    }
                }
                else {
                    throw new Error('Newly renamed character was lost?');
                }
            }
            else {
                throw new Error('Could not rename the character');
            }
        }
        catch {
            // Reloading to prevent data corruption
            await callPopup('Something went wrong. The page will be reloaded.', 'text');
            location.reload();
        }
    }
}

async function renamePastChats(newAvatar, newValue) {
    const pastChats = await getPastCharacterChats();

    for (const { file_name } of pastChats) {
        try {
            const fileNameWithoutExtension = file_name.replace('.jsonl', '');
            const getChatResponse = await fetch('/getchat', {
                method: 'POST',
                headers: getRequestHeaders(),
                body: JSON.stringify({
                    ch_name: newValue,
                    file_name: fileNameWithoutExtension,
                    avatar_url: newAvatar,
                }),
                cache: 'no-cache',
            });

            if (getChatResponse.ok) {
                const currentChat = await getChatResponse.json();

                for (const message of currentChat) {
                    if (message.is_user || message.is_system || message.extra?.type == system_message_types.NARRATOR) {
                        continue;
                    }

                    if (message.name !== undefined) {
                        message.name = newValue;
                    }
                }

                const saveChatResponse = await fetch('/savechat', {
                    method: "POST",
                    headers: getRequestHeaders(),
                    body: JSON.stringify({
                        ch_name: newValue,
                        file_name: fileNameWithoutExtension,
                        chat: currentChat,
                        avatar_url: newAvatar,
                    }),
                    cache: 'no-cache',
                });

                if (!saveChatResponse.ok) {
                    throw new Error('Could not save chat');
                }
            }
        } catch (error) {
            toastr.error(`Past chat could not be updated: ${file_name}`);
            console.error(error);
        }
    }
}

function saveChatDebounced() {
    const chid = this_chid;
    const selectedGroup = selected_group;

    if (chatSaveTimeout) {
        console.debug('Clearing chat save timeout');
        clearTimeout(chatSaveTimeout);
    }

    chatSaveTimeout = setTimeout(async () => {
        if (selectedGroup !== selected_group) {
            console.warn('Chat save timeout triggered, but group changed. Aborting.');
            return;
        }

        if (chid !== this_chid) {
            console.warn('Chat save timeout triggered, but chid changed. Aborting.');
            return;
        }

        console.debug('Chat save timeout triggered');
        await saveChatConditional();
        console.debug('Chat saved');
    }, 1000);
}

async function saveChat(chat_name, withMetadata, mesId) {
    const metadata = { ...chat_metadata, ...(withMetadata || {}) };
    let file_name = chat_name ?? characters[this_chid].chat;
    characters[this_chid]['date_last_chat'] = Date.now();
    chat.forEach(function (item, i) {
        if (item["is_group"]) {
            toastr.error('Trying to save group chat with regular saveChat function. Aborting to prevent corruption.');
            throw new Error('Group chat saved from saveChat');
        }
        /*
        if (item.is_user) {
            //var str = item.mes.replace(`${name1}:`, `${name1}:`);
            //chat[i].mes = str;
            //chat[i].name = name1;
        } else if (i !== chat.length - 1 && chat[i].swipe_id !== undefined) {
            //  delete chat[i].swipes;
            //  delete chat[i].swipe_id;
        }
        */
    });

    const trimmed_chat = (mesId !== undefined && mesId >= 0 && mesId < chat.length)
        ? chat.slice(0, parseInt(mesId) + 1)
        : chat;

    var save_chat = [
        {
            user_name: name1,
            character_name: name2,
            create_date: chat_create_date,
            chat_metadata: metadata,
        },
        ...trimmed_chat,
    ];
    return jQuery.ajax({
        type: "POST",
        url: "/savechat",
        data: JSON.stringify({
            ch_name: characters[this_chid].name,
            file_name: file_name,
            chat: save_chat,
            avatar_url: characters[this_chid].avatar,
        }),
        beforeSend: function () {

        },
        cache: false,
        dataType: "json",
        contentType: "application/json",
        success: function (data) { },
        error: function (jqXHR, exception) {
            console.log(exception);
            console.log(jqXHR);
        },
    });
}

async function read_avatar_load(input) {
    if (input.files && input.files[0]) {
        if (selected_button == "create") {
            create_save.avatar = input.files;
        }

        const e = await new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = resolve;
            reader.onerror = reject;
            reader.readAsDataURL(input.files[0]);
        })

        $('#dialogue_popup').addClass('large_dialogue_popup wide_dialogue_popup');

        const croppedImage = await callPopup(getCropPopup(e.target.result), 'avatarToCrop');
        if (!croppedImage) {
            return;
        }

        $("#avatar_load_preview").attr("src", croppedImage || e.target.result);

        if (menu_type == "create") {
            return;
        }

        await createOrEditCharacter();
        await delay(durationSaveEdit);

        const formData = new FormData($("#form_create").get(0));
        await fetch(getThumbnailUrl('avatar', formData.get('avatar_url')), {
            method: 'GET',
            cache: 'no-cache',
            headers: {
                'pragma': 'no-cache',
                'cache-control': 'no-cache',
            }
        });

        $(".mes").each(async function () {
            const nameMatch = $(this).attr("ch_name") == formData.get('ch_name');
            if ($(this).attr("is_system") == 'true' && !nameMatch) {
                return;
            }
            if ($(this).attr("is_user") == 'true') {
                return;
            }
            if (nameMatch) {
                const previewSrc = $("#avatar_load_preview").attr("src");
                const avatar = $(this).find(".avatar img");
                avatar.attr('src', default_avatar);
                await delay(1);
                avatar.attr('src', previewSrc);
            }
        });

        console.log('Avatar refreshed');
    }
}

export function getCropPopup(src) {
    return `<h3>Set the crop position of the avatar image and click Accept to confirm.</h3>
            <div id='avatarCropWrap'>
                <img id='avatarToCrop' src='${src}'>
            </div>`;
}

function getThumbnailUrl(type, file) {
    return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}`;
}

async function getChat() {
    //console.log('/getchat -- entered for -- ' + characters[this_chid].name);
    try {
        const response = await $.ajax({
            type: 'POST',
            url: '/getchat',
            data: JSON.stringify({
                ch_name: characters[this_chid].name,
                file_name: characters[this_chid].chat,
                avatar_url: characters[this_chid].avatar
            }),
            dataType: 'json',
            contentType: 'application/json',
        });
        if (response[0] !== undefined) {
            chat.push(...response);
            chat_create_date = chat[0]['create_date'];
            chat_metadata = chat[0]['chat_metadata'] ?? {};

            chat.shift();
        } else {
            chat_create_date = humanizedDateTime();
        }
        await getChatResult();
        eventSource.emit('chatLoaded', { detail: { id: this_chid, character: characters[this_chid] } });

        setTimeout(function () {
            $('#send_textarea').click();
            $('#send_textarea').focus();
        }, 200);
    } catch (error) {
        await getChatResult();
        console.log(error);
    }
}

async function getChatResult() {
    name2 = characters[this_chid].name;
    if (chat.length === 0) {
        const message = getFirstMessage();
        chat.push(message);
        await saveChatConditional();
    }
    await printMessages();
    select_selected_character(this_chid);

    await eventSource.emit(event_types.CHAT_CHANGED, (getCurrentChatId()));

    if (chat.length === 1) {
        const chat_id = (chat.length - 1);
        await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
        await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id);
    }
}

function getFirstMessage() {
    const firstMes = characters[this_chid].first_mes || default_ch_mes;
    const alternateGreetings = characters[this_chid]?.data?.alternate_greetings;

    const message = {
        name: name2,
        is_user: false,
        is_system: false,
        send_date: getMessageTimeStamp(),
        mes: getRegexedString(firstMes, regex_placement.AI_OUTPUT),
        extra: {},
    };

    if (Array.isArray(alternateGreetings) && alternateGreetings.length > 0) {
        const swipes = [message.mes, ...(alternateGreetings.map(greeting => substituteParams(getRegexedString(greeting, regex_placement.AI_OUTPUT))))];
        message['swipe_id'] = 0;
        message['swipes'] = swipes;
        message['swipe_info'] = [];
    }
    return message;
}

async function openCharacterChat(file_name) {
    characters[this_chid]["chat"] = file_name;
    clearChat();
    chat.length = 0;
    chat_metadata = {};
    await getChat();
    $("#selected_chat_pole").val(file_name);
    await createOrEditCharacter();
}

////////// OPTIMZED MAIN API CHANGE FUNCTION ////////////

function changeMainAPI() {
    const selectedVal = $("#main_api").val();
    //console.log(selectedVal);
    const apiElements = {
        "koboldhorde": {
            apiSettings: $("#kobold_api-settings"),
            apiConnector: $("#kobold_horde"),
            apiPresets: $('#kobold_api-presets'),
            apiRanges: $("#range_block"),
            maxContextElem: $("#max_context_block"),
            amountGenElem: $("#amount_gen_block"),
        },
        "kobold": {
            apiSettings: $("#kobold_api-settings"),
            apiConnector: $("#kobold_api"),
            apiPresets: $('#kobold_api-presets'),
            apiRanges: $("#range_block"),
            maxContextElem: $("#max_context_block"),
            amountGenElem: $("#amount_gen_block"),
        },
        "textgenerationwebui": {
            apiSettings: $("#textgenerationwebui_api-settings"),
            apiConnector: $("#textgenerationwebui_api"),
            apiPresets: $('#textgenerationwebui_api-presets'),
            apiRanges: $("#range_block_textgenerationwebui"),
            maxContextElem: $("#max_context_block"),
            amountGenElem: $("#amount_gen_block"),
        },
        "novel": {
            apiSettings: $("#novel_api-settings"),
            apiConnector: $("#novel_api"),
            apiPresets: $('#novel_api-presets'),
            apiRanges: $("#range_block_novel"),
            maxContextElem: $("#max_context_block"),
            amountGenElem: $("#amount_gen_block"),
        },
        "openai": {
            apiSettings: $("#openai_settings"),
            apiConnector: $("#openai_api"),
            apiPresets: $('#openai_api-presets'),
            apiRanges: $("#range_block_openai"),
            maxContextElem: $("#max_context_block"),
            amountGenElem: $("#amount_gen_block"),
        }
    };
    //console.log('--- apiElements--- ');
    //console.log(apiElements);

    //first, disable everything so the old elements stop showing
    for (const apiName in apiElements) {
        const apiObj = apiElements[apiName];
        //do not hide items to then proceed to immediately show them.
        if (selectedVal === apiName) {
            continue;
        }
        apiObj.apiSettings.css("display", "none");
        apiObj.apiConnector.css("display", "none");
        apiObj.apiRanges.css("display", "none");
        apiObj.apiPresets.css("display", "none");
    }

    //then, find and enable the active item.
    //This is split out of the loop so that different apis can share settings divs
    let activeItem = apiElements[selectedVal];

    activeItem.apiSettings.css("display", "block");
    activeItem.apiConnector.css("display", "block");
    activeItem.apiRanges.css("display", "block");
    activeItem.apiPresets.css("display", "block");

    if (selectedVal === "openai") {
        activeItem.apiPresets.css("display", "flex");
    }

    if (selectedVal === "textgenerationwebui" || selectedVal === "novel") {
        console.log("enabling amount_gen for ooba/novel");
        activeItem.amountGenElem.find('input').prop("disabled", false);
        activeItem.amountGenElem.css("opacity", 1.0);
    }

    if (selectedVal === "novel") {
        $("#ai_module_block_novel").css("display", "block");
    } else {
        $("#ai_module_block_novel").css("display", "none");
    }

    // Hide common settings for OpenAI
    console.debug('value?', selectedVal);
    if (selectedVal == "openai") {
        console.debug('hiding settings?');
        $("#common-gen-settings-block").css("display", "none");
    } else {
        $("#common-gen-settings-block").css("display", "block");
    }

    main_api = selectedVal;
    online_status = "no_connection";

    if (main_api == 'openai' && oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI) {
        $('#api_button_openai').trigger('click');
    }

    if (main_api == "koboldhorde") {
        is_get_status = true;
        getStatus();
        getHordeModels();
    }

    switch (oai_settings.chat_completion_source) {
        case chat_completion_sources.SCALE:
        case chat_completion_sources.OPENROUTER:
        case chat_completion_sources.WINDOWAI:
        case chat_completion_sources.CLAUDE:
        case chat_completion_sources.OPENAI:
        case chat_completion_sources.AI21:
        case chat_completion_sources.PALM:
        default:
            setupChatCompletionPromptManager(oai_settings);
            break;
    }
}

////////////////////////////////////////////////////

export async function getUserAvatars() {
    const response = await fetch("/getuseravatars", {
        method: "POST",
        headers: getRequestHeaders(),
        body: JSON.stringify({
            "": "",
        }),
    });
    if (response.ok === true) {
        const getData = await response.json();
        $("#user_avatar_block").html(""); //RossAscends: necessary to avoid doubling avatars each refresh.
        $("#user_avatar_block").append('<div class="avatar_upload">+</div>');

        for (var i = 0; i < getData.length; i++) {
            //console.log(1);
            appendUserAvatar(getData[i]);
        }
        //var aa = JSON.parse(getData[0]);
        //const load_ch_coint = Object.getOwnPropertyNames(getData);
    }
}

function highlightSelectedAvatar() {
    $("#user_avatar_block").find(".avatar").removeClass("selected");
    $("#user_avatar_block")
        .find(`.avatar[imgfile='${user_avatar}']`)
        .addClass("selected");
}

function appendUserAvatar(name) {
    const template = $('#user_avatar_template .avatar-container').clone();
    const personaName = power_user.personas[name];
    if (personaName) {
        template.attr('title', personaName);
    } else {
        template.attr('title', '[Unnamed Persona]');
    }
    template.find('.avatar').attr('imgfile', name);
    template.toggleClass('default_persona', name === power_user.default_persona)
    template.find('img').attr('src', getUserAvatar(name));
    $("#user_avatar_block").append(template);
    highlightSelectedAvatar();
}

function reloadUserAvatar(force = false) {
    $(".mes").each(function () {
        const avatarImg = $(this).find(".avatar img");
        if (force) {
            avatarImg.attr("src", avatarImg.attr("src"));
        }

        if ($(this).attr("is_user") == 'true' && $(this).attr('force_avatar') == 'false') {
            avatarImg.attr("src", getUserAvatar(user_avatar));
        }
    });
}

export function setUserName(value) {
    name1 = value;
    if (name1 === undefined || name1 == "")
        name1 = default_user_name;
    console.log(`User name changed to ${name1}`);
    $("#your_name").val(name1);
    if (power_user.persona_show_notifications) {
        toastr.success(`Your messages will now be sent as ${name1}`, 'Current persona updated');
    }
    saveSettings("change_name");
}

function setUserAvatar() {
    user_avatar = $(this).attr("imgfile");
    reloadUserAvatar();
    highlightSelectedAvatar();
    selectCurrentPersona();
    saveSettingsDebounced();
    $('.zoomed_avatar[forchar]').remove();
}

async function uploadUserAvatar(e) {
    const file = e.target.files[0];

    if (!file) {
        $("#form_upload_avatar").trigger("reset");
        return;
    }

    const formData = new FormData($("#form_upload_avatar").get(0));

    const dataUrl = await new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = resolve;
        reader.onerror = reject;
        reader.readAsDataURL(file);
    });

    $('#dialogue_popup').addClass('large_dialogue_popup wide_dialogue_popup');
    const confirmation = await callPopup(getCropPopup(dataUrl.target.result), 'avatarToCrop');
    if (!confirmation) {
        return;
    }

    let url = "/uploaduseravatar";

    if (crop_data !== undefined) {
        url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`;
    }

    jQuery.ajax({
        type: "POST",
        url: url,
        data: formData,
        beforeSend: () => { },
        cache: false,
        contentType: false,
        processData: false,
        success: async function () {
            // If the user uploaded a new avatar, we want to make sure it's not cached
            const name = formData.get("overwrite_name");
            if (name) {
                await fetch(getUserAvatar(name), { cache: "no-cache" });
                reloadUserAvatar(true);
            }

            crop_data = undefined;
            await getUserAvatars();
        },
        error: (jqXHR, exception) => { },
    });

    // Will allow to select the same file twice in a row
    $("#form_upload_avatar").trigger("reset");
}

async function doOnboarding(avatarId) {
    let simpleUiMode = false;
    const template = $('#onboarding_template .onboarding');
    template.find('input[name="enable_simple_mode"]').on('input', function () {
        simpleUiMode = $(this).is(':checked');
    });
    const userName = await callPopup(template, 'input', name1);

    if (userName) {
        setUserName(userName);
        console.log(`Binding persona ${avatarId} to name ${userName}`);
        power_user.personas[avatarId] = userName;
        power_user.persona_descriptions[avatarId] = {
            description: '',
            position: persona_description_positions.IN_PROMPT,
        };
    }

    if (simpleUiMode) {
        power_user.ui_mode = ui_mode.SIMPLE;
        $('#ui_mode_select').val(power_user.ui_mode);
        switchSimpleMode();
    }
}

//***************SETTINGS****************//
///////////////////////////////////////////
async function getSettings(type) {
    const response = await fetch("/getsettings", {
        method: "POST",
        headers: getRequestHeaders(),
        body: JSON.stringify({}),
        cache: "no-cache",
    });

    if (!response.ok) {
        toastr.error('Settings could not be loaded. Try reloading the page.');
        throw new Error('Error getting settings');
    }

    const data = await response.json();
    if (data.result != "file not find" && data.settings) {
        settings = JSON.parse(data.settings);
        if (settings.username !== undefined && settings.username !== "") {
            name1 = settings.username;
            $("#your_name").val(name1);
        }

        // Allow subscribers to mutate settings
        eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings);

        //Load KoboldAI settings
        koboldai_setting_names = data.koboldai_setting_names;
        koboldai_settings = data.koboldai_settings;
        koboldai_settings.forEach(function (item, i, arr) {
            koboldai_settings[i] = JSON.parse(item);
        });

        let arr_holder = {};

        $("#settings_perset").empty(); //RossAscends: uncommented this to prevent settings selector from doubling preset list on refresh
        $("#settings_perset").append(
            '<option value="gui">GUI KoboldAI Settings</option>'
        ); //adding in the GUI settings, since it is not loaded dynamically

        koboldai_setting_names.forEach(function (item, i, arr) {
            arr_holder[item] = i;
            $("#settings_perset").append(`<option value=${i}>${item}</option>`);
            //console.log('loading preset #'+i+' -- '+item);
        });
        koboldai_setting_names = {};
        koboldai_setting_names = arr_holder;
        preset_settings = settings.preset_settings;

        if (preset_settings == "gui") {
            selectKoboldGuiPreset();
        } else {
            if (typeof koboldai_setting_names[preset_settings] !== "undefined") {
                $(`#settings_perset option[value=${koboldai_setting_names[preset_settings]}]`)
                    .attr("selected", "true");
            } else {
                preset_settings = "gui";
                selectKoboldGuiPreset();
            }
        }

        novelai_setting_names = data.novelai_setting_names;
        novelai_settings = data.novelai_settings;
        novelai_settings.forEach(function (item, i, arr) {
            novelai_settings[i] = JSON.parse(item);
        });
        arr_holder = {};

        $("#settings_perset_novel").empty();

        novelai_setting_names.forEach(function (item, i, arr) {
            arr_holder[item] = i;
            $("#settings_perset_novel").append(`<option value=${i}>${item}</option>`);
        });
        novelai_setting_names = {};
        novelai_setting_names = arr_holder;

        //Load AI model config settings

        amount_gen = settings.amount_gen;
        if (settings.max_context !== undefined)
            max_context = parseInt(settings.max_context);

        swipes = settings.swipes !== undefined ? !!settings.swipes : true;  // enable swipes by default
        $('#swipes-checkbox').prop('checked', swipes); /// swipecode
        hideSwipeButtons();
        showSwipeButtons();

        // Kobold
        loadKoboldSettings(settings.kai_settings ?? settings);

        // Novel
        loadNovelSettings(settings.nai_settings ?? settings);
        $(`#settings_perset_novel option[value=${novelai_setting_names[nai_settings.preset_settings_novel]}]`).attr("selected", "true");

        // TextGen
        loadTextGenSettings(data, settings);

        // OpenAI
        loadOpenAISettings(data, settings.oai_settings ?? settings);

        // Horde
        loadHordeSettings(settings);

        // Load power user settings
        loadPowerUserSettings(settings, data);

        // Load character tags
        loadTagsSettings(settings);

        // Allow subscribers to mutate settings
        eventSource.emit(event_types.SETTINGS_LOADED_AFTER, settings);

        // Set context size after loading power user (may override the max value)
        $("#max_context").val(max_context);
        $("#max_context_counter").val(max_context);

        $("#amount_gen").val(amount_gen);
        $("#amount_gen_counter").val(amount_gen);

        //Load which API we are using
        if (settings.main_api == undefined) {
            settings.main_api = 'kobold';
        }

        if (settings.main_api == 'poe') {
            settings.main_api = 'openai';
        }

        main_api = settings.main_api;
        $('#main_api').val(main_api);
        $("#main_api option[value=" + main_api + "]").attr(
            "selected",
            "true"
        );
        changeMainAPI();

        //Load User's Name and Avatar

        user_avatar = settings.user_avatar;
        firstRun = !!settings.firstRun;

        if (firstRun) {
            await doOnboarding(user_avatar);
            firstRun = false;
        }

        reloadUserAvatar();
        highlightSelectedAvatar();
        setPersonaDescription();

        //Load the active character and group
        active_character = settings.active_character;
        active_group = settings.active_group;

        //Load the API server URL from settings
        api_server = settings.api_server;
        $("#api_url_text").val(api_server);

        setWorldInfoSettings(settings.world_info_settings ?? settings, data);

        api_server_textgenerationwebui = settings.api_server_textgenerationwebui;
        $("#textgenerationwebui_api_url_text").val(api_server_textgenerationwebui);
        $("#mancer_api_url_text").val(api_server_textgenerationwebui);
        $("#aphrodite_api_url_text").val(api_server_textgenerationwebui);

        selected_button = settings.selected_button;

        if (data.enable_extensions) {
            const isVersionChanged = settings.currentVersion !== currentVersion;
            await loadExtensionSettings(settings, isVersionChanged);
            eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED);
        }
    }

    eventSource.emit(event_types.SETTINGS_LOADED);
}

function selectKoboldGuiPreset() {
    $("#settings_perset option[value=gui]")
        .attr("selected", "true")
        .trigger("change");
}

async function saveSettings(type) {
    if (!settingsReady) {
        console.warn('Settings not ready, aborting save');
        return;
    }
    //console.log('Entering settings with name1 = '+name1);

    return jQuery.ajax({
        type: "POST",
        url: "/savesettings",
        data: JSON.stringify({
            firstRun: firstRun,
            currentVersion: currentVersion,
            username: name1,
            active_character: active_character,
            active_group: active_group,
            api_server: api_server,
            api_server_textgenerationwebui: api_server_textgenerationwebui,
            preset_settings: preset_settings,
            user_avatar: user_avatar,
            amount_gen: amount_gen,
            max_context: max_context,
            main_api: main_api,
            world_info_settings: getWorldInfoSettings(),
            textgenerationwebui_settings: textgenerationwebui_settings,
            swipes: swipes,
            horde_settings: horde_settings,
            power_user: power_user,
            extension_settings: extension_settings,
            tags: tags,
            tag_map: tag_map,
            nai_settings: nai_settings,
            kai_settings: kai_settings,
            oai_settings: oai_settings,
        }, null, 4),
        beforeSend: function () {
            if (type == "change_name") {
                //let nameBeforeChange = name1;
                name1 = $("#your_name").val();
                //$(`.mes[ch_name="${nameBeforeChange}"]`).attr('ch_name' === name1);
                //console.log('beforeSend name1 = ' + nameBeforeChange);
                //console.log('new name: ' + name1);
            }
        },
        cache: false,
        dataType: "json",
        contentType: "application/json",
        //processData: false,
        success: async function (data) {
            //online_status = data.result;
            eventSource.emit(event_types.SETTINGS_UPDATED);
            if (type == "change_name") {
                clearChat();
                await printMessages();
            }
        },
        error: function (jqXHR, exception) {
            toastr.error('Check the server connection and reload the page to prevent data loss.', 'Settings could not be saved');
            console.log(exception);
            console.log(jqXHR);
        },
    });
}

export function setGenerationParamsFromPreset(preset) {
    if (preset.genamt !== undefined) {
        amount_gen = preset.genamt;
        $("#amount_gen").val(amount_gen);
        $("#amount_gen_counter").val(amount_gen);
    }

    if (preset.max_length !== undefined) {
        const needsUnlock = preset.max_length > MAX_CONTEXT_DEFAULT;
        $('#max_context_unlocked').prop('checked', needsUnlock).trigger('change');
        max_context = preset.max_length;

        $("#max_context").val(max_context);
        $("#max_context_counter").val(max_context);
    }
}

// Common code for message editor done and auto-save
function updateMessage(div) {
    const mesBlock = div.closest(".mes_block");
    let text = mesBlock.find(".edit_textarea").val();
    const mes = chat[this_edit_mes_id];

    let regexPlacement;
    if (mes.is_user) {
        regexPlacement = regex_placement.USER_INPUT;
    } else if (mes.name === name2) {
        regexPlacement = regex_placement.AI_OUTPUT;
    } else if (mes.name !== name2 || mes.extra?.type === "narrator") {
        regexPlacement = regex_placement.SLASH_COMMAND;
    }

    // Ignore character override if sent as system
    text = getRegexedString(
        text,
        regexPlacement,
        { characterOverride: mes.extra?.type === "narrator" ? undefined : mes.name }
    );


    if (power_user.trim_spaces) {
        text = text.trim();
    }

    const bias = extractMessageBias(text);
    mes["mes"] = text;
    if (mes["swipe_id"] !== undefined) {
        mes["swipes"][mes["swipe_id"]] = text;
    }

    // editing old messages
    if (!mes.extra) {
        mes.extra = {};
    }

    if (mes.is_system || mes.is_user || mes.extra.type === system_message_types.NARRATOR) {
        mes.extra.bias = bias ?? null;
    } else {
        mes.extra.bias = null;
    }

    return { mesBlock, text, mes, bias };
}

function openMessageDelete() {
    closeMessageEditor();
    hideSwipeButtons();
    if ((this_chid != undefined && !is_send_press) || (selected_group && !is_group_generating)) {
        $("#dialogue_del_mes").css("display", "block");
        $("#send_form").css("display", "none");
        $(".del_checkbox").each(function () {
            if ($(this).parent().attr("mesid") != 0) {
                $(this).css("display", "block");
                $(this).parent().children(".for_checkbox").css("display", "none");
            }
        });
    } else {
        console.debug(`
            ERR -- could not enter del mode
            this_chid: ${this_chid}
            is_send_press: ${is_send_press}
            selected_group: ${selected_group}
            is_group_generating: ${is_group_generating}`);
    }
    is_delete_mode = true;
}

function messageEditAuto(div) {
    const { mesBlock, text, mes } = updateMessage(div);

    mesBlock.find(".mes_text").val('');
    mesBlock.find(".mes_text").val(messageFormatting(
        text,
        this_edit_mes_chname,
        mes.is_system,
        mes.is_user,
    ));
    saveChatDebounced();
}

async function messageEditDone(div) {
    let { mesBlock, text, mes, bias } = updateMessage(div);
    if (this_edit_mes_id == 0) {
        text = substituteParams(text);
    }

    mesBlock.find(".mes_text").empty();
    mesBlock.find(".mes_edit_buttons").css("display", "none");
    mesBlock.find(".mes_buttons").css("display", "");
    mesBlock.find(".mes_text").append(
        messageFormatting(
            text,
            this_edit_mes_chname,
            mes.is_system,
            mes.is_user,
        )
    );
    mesBlock.find(".mes_bias").empty();
    mesBlock.find(".mes_bias").append(messageFormatting(bias));
    appendImageToMessage(mes, div.closest(".mes"));
    addCopyToCodeBlocks(div.closest(".mes"));
    await eventSource.emit(event_types.MESSAGE_EDITED, this_edit_mes_id);

    this_edit_mes_id = undefined;
    await saveChatConditional();
}

/**
 * Fetches the chat content for each chat file from the server and compiles them into a dictionary.
 * The function iterates over a provided list of chat metadata and requests the actual chat content
 * for each chat, either as an individual chat or a group chat based on the context.
 *
 * @param {Array} data - An array containing metadata about each chat such as file_name.
 * @param {boolean} isGroupChat - A flag indicating if the chat is a group chat.
 * @returns {Promise<Object>} chat_dict - A dictionary where each key is a file_name and the value is the
 * corresponding chat content fetched from the server.
 */
export async function getChatsFromFiles(data, isGroupChat) {
    const context = getContext();
    let chat_dict = {};
    let chat_list = Object.values(data).sort((a, b) => a["file_name"].localeCompare(b["file_name"])).reverse();

    let chat_promise = chat_list.map(({ file_name }) => {
        return new Promise(async (res, rej) => {
            try {
                const endpoint = isGroupChat ? '/getgroupchat' : '/getchat';
                const requestBody = isGroupChat
                    ? JSON.stringify({ id: file_name })
                    : JSON.stringify({
                        ch_name: characters[context.characterId].name,
                        file_name: file_name.replace('.jsonl', ''),
                        avatar_url: characters[context.characterId].avatar
                    });

                const chatResponse = await fetch(endpoint, {
                    method: 'POST',
                    headers: getRequestHeaders(),
                    body: requestBody,
                    cache: 'no-cache',
                });

                if (!chatResponse.ok) {
                    return res();
                    // continue;
                }

                const currentChat = await chatResponse.json();
                if (!isGroupChat) {
                    // remove the first message, which is metadata, only for individual chats
                    currentChat.shift();
                }
                chat_dict[file_name] = currentChat;

            } catch (error) {
                console.error(error);
            }

            return res();
        })
    })

    await Promise.all(chat_promise)

    return chat_dict;
}

/**
 * Fetches the metadata of all past chats related to a specific character based on its avatar URL.
 * The function sends a POST request to the server to retrieve all chats for the character. It then
 * processes the received data, sorts it by the file name, and returns the sorted data.
 *
 * @returns {Promise<Array>} - An array containing metadata of all past chats of the character, sorted
 * in descending order by file name. Returns `undefined` if the fetch request is unsuccessful.
 */
async function getPastCharacterChats() {
    const response = await fetch("/getallchatsofcharacter", {
        method: 'POST',
        body: JSON.stringify({ avatar_url: characters[this_chid].avatar }),
        headers: getRequestHeaders(),
    });

    if (!response.ok) {
        return;
    }

    let data = await response.json();
    data = Object.values(data);
    data = data.sort((a, b) => a["file_name"].localeCompare(b["file_name"])).reverse();
    return data;
}

/**
 * Displays the past chats for a character or a group based on the selected context.
 * The function first fetches the chats, processes them, and then displays them in
 * the HTML. It also has a built-in search functionality that allows filtering the
 * displayed chats based on a search query.
 */
export async function displayPastChats() {
    $("#select_chat_div").empty();

    const group = selected_group ? groups.find(x => x.id === selected_group) : null;
    const data = await (selected_group ? getGroupPastChats(selected_group) : getPastCharacterChats());
    const currentChat = selected_group ? group?.chat_id : characters[this_chid]["chat"];
    const displayName = selected_group ? group?.name : characters[this_chid].name;
    const avatarImg = selected_group ? group?.avatar_url : getThumbnailUrl('avatar', characters[this_chid]['avatar']);
    const rawChats = await getChatsFromFiles(data, selected_group);
    // Sort by last message date descending
    data.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes)));
    console.log(data);
    $("#load_select_chat_div").css("display", "none");
    $("#ChatHistoryCharName").text(displayName);

    const displayChats = (searchQuery) => {
        $("#select_chat_div").empty();  // Clear the current chats before appending filtered chats

        const filteredData = data.filter(chat => {
            const fileName = chat['file_name'];
            const chatContent = rawChats[fileName];

            return chatContent && Object.values(chatContent).some(message => message?.mes?.toLowerCase()?.includes(searchQuery.toLowerCase()));
        });

        console.log(filteredData);
        for (const key in filteredData) {
            let strlen = 300;
            let mes = filteredData[key]["mes"];

            if (mes !== undefined) {
                if (mes.length > strlen) {
                    mes = "..." + mes.substring(mes.length - strlen);
                }
                const chat_items = data[key]["chat_items"];
                const file_size = data[key]["file_size"];
                const fileName = data[key]['file_name'];
                const timestamp = timestampToMoment(data[key]['last_mes']).format('LL LT');
                const template = $('#past_chat_template .select_chat_block_wrapper').clone();
                template.find('.select_chat_block').attr('file_name', fileName);
                template.find('.avatar img').attr('src', avatarImg);
                template.find('.select_chat_block_filename').text(fileName);
                template.find('.chat_file_size').text(" (" + file_size + ")");
                template.find('.chat_messages_num').text(" (" + chat_items + " messages)");
                template.find('.select_chat_block_mes').text(mes);
                template.find('.PastChat_cross').attr('file_name', fileName);
                template.find('.chat_messages_date').text(timestamp);

                if (selected_group) {
                    template.find('.avatar img').replaceWith(getGroupAvatar(group));
                }

                $("#select_chat_div").append(template);

                if (currentChat === fileName.toString().replace(".jsonl", "")) {
                    $("#select_chat_div").find(".select_chat_block:last").attr("highlight", true);
                }
            }
        }
    }
    displayChats('');  // Display all by default

    const debouncedDisplay = debounce((searchQuery) => {
        displayChats(searchQuery);
    }, 300);

    // Define the search input listener
    $("#select_chat_search").on("input", function () {
        const searchQuery = $(this).val();
        debouncedDisplay(searchQuery);
    });
}

//************************************************************
//************************Novel.AI****************************
//************************************************************
async function getStatusNovel() {
    if (is_get_status_novel) {
        try {
            const result = await loadNovelSubscriptionData();

            if (!result) {
                throw new Error('Could not load subscription data');
            }

            online_status = getNovelTier();
        } catch {
            online_status = "no_connection";
        }

        resultCheckStatusNovel();
    } else {
        if (is_get_status != true && is_get_status_openai != true) {
            online_status = "no_connection";
        }
    }
}

function selectRightMenuWithAnimation(selectedMenuId) {
    const displayModes = {
        'rm_group_chats_block': 'flex',
        'rm_api_block': 'grid',
        'rm_characters_block': 'flex',
    };
    $('#result_info').toggle(selectedMenuId === 'rm_ch_create_block');
    document.querySelectorAll('#right-nav-panel .right_menu').forEach((menu) => {
        $(menu).css('display', 'none');

        if (selectedMenuId && selectedMenuId.replace('#', '') === menu.id) {
            const mode = displayModes[menu.id] ?? 'block';
            $(menu).css('display', mode);
            $(menu).css("opacity", 0.0);
            $(menu).transition({
                opacity: 1.0,
                duration: animation_duration,
                easing: animation_easing,
                complete: function () { },
            });
        }
    })
}

function select_rm_info(type, charId, previousCharId = null) {
    if (!type) {
        toastr.error(`Invalid process (no 'type')`);
        return;
    }
    if (type !== 'group_create') {
        var displayName = String(charId).replace('.png', '');
    }

    if (type === 'char_delete') {
        toastr.warning(`Character Deleted: ${displayName}`);
    }
    if (type === 'char_create') {
        toastr.success(`Character Created: ${displayName}`);
    }
    if (type === 'group_create') {
        toastr.success(`Group Created`);
    }
    if (type === 'group_delete') {
        toastr.warning(`Group Deleted`);
    }

    if (type === 'char_import') {
        toastr.success(`Character Imported: ${displayName}`);
    }

    selectRightMenuWithAnimation('rm_characters_block');

    // Set a timeout so multiple flashes don't overlap
    clearTimeout(importFlashTimeout);
    importFlashTimeout = setTimeout(function () {
        if (type === 'char_import' || type === 'char_create') {
            // Find the page at which the character is located
            const charData = getEntitiesList({ doFilter: true });
            const charIndex = charData.findIndex((x) => x?.item?.avatar?.startsWith(charId));

            if (charIndex === -1) {
                console.log(`Could not find character ${charId} in the list`);
                return;
            }

            try {
                const perPage = Number(localStorage.getItem('Characters_PerPage')) || per_page_default;
                const page = Math.floor(charIndex / perPage) + 1;
                const selector = `#rm_print_characters_block [title^="${charId}"]`;
                $('#rm_print_characters_pagination').pagination('go', page);

                waitUntilCondition(() => document.querySelector(selector) !== null).then(() => {
                    const element = $(selector).parent();

                    if (element.length === 0) {
                        console.log(`Could not find element for character ${charId}`);
                        return;
                    }

                    const scrollOffset = element.offset().top - element.parent().offset().top;
                    element.parent().scrollTop(scrollOffset);
                    element.addClass('flash animated');
                    setTimeout(function () {
                        element.removeClass('flash animated');
                    }, 5000);
                });
            } catch (e) {
                console.error(e);
            }
        }

        if (type === 'group_create') {
            // Find the page at which the character is located
            const charData = getEntitiesList({ doFilter: true });
            const charIndex = charData.findIndex((x) => String(x?.item?.id) === String(charId));

            if (charIndex === -1) {
                console.log(`Could not find group ${charId} in the list`);
                return;
            }

            const perPage = Number(localStorage.getItem('Characters_PerPage')) || per_page_default;
            const page = Math.floor(charIndex / perPage) + 1;
            $('#rm_print_characters_pagination').pagination('go', page);
            const selector = `#rm_print_characters_block [grid="${charId}"]`;
            try {
                waitUntilCondition(() => document.querySelector(selector) !== null).then(() => {
                    const element = $(selector);
                    const scrollOffset = element.offset().top - element.parent().offset().top;
                    element.parent().scrollTop(scrollOffset);
                    $(element).addClass('flash animated');
                    setTimeout(function () {
                        $(element).removeClass('flash animated');
                    }, 5000);
                });
            } catch (e) {
                console.error(e);
            }
        }
    }, 250);

    if (previousCharId) {
        const newId = characters.findIndex((x) => x.avatar == previousCharId);
        if (newId >= 0) {
            this_chid = newId;
        }
    }
}

export function select_selected_character(chid) {
    //character select
    //console.log('select_selected_character() -- starting with input of -- '+chid+' (name:'+characters[chid].name+')');
    select_rm_create();
    menu_type = "character_edit";
    $("#delete_button").css("display", "flex");
    $("#export_button").css("display", "flex");
    var display_name = characters[chid].name;

    //create text poles
    $("#rm_button_back").css("display", "none");
    //$("#character_import_button").css("display", "none");
    $("#create_button").attr("value", "Save");              // what is the use case for this?
    $("#dupe_button").show();
    $("#create_button_label").css("display", "none");

    // Hide the chat scenario button if we're peeking the group member defs
    $('#set_chat_scenario').toggle(!selected_group);

    // Don't update the navbar name if we're peeking the group member defs
    if (!selected_group) {
        $("#rm_button_selected_ch").children("h2").text(display_name);
    }

    $("#add_avatar_button").val("");

    $("#character_popup_text_h3").text(characters[chid].name);
    $("#character_name_pole").val(characters[chid].name);
    $("#description_textarea").val(characters[chid].description);
    $("#character_world").val(characters[chid].data?.extensions?.world || '');
    $("#creator_notes_textarea").val(characters[chid].data?.creator_notes || characters[chid].creatorcomment);
    $("#creator_notes_spoiler").text(characters[chid].data?.creator_notes || characters[chid].creatorcomment);
    $("#character_version_textarea").val(characters[chid].data?.character_version || '');
    $("#system_prompt_textarea").val(characters[chid].data?.system_prompt || '');
    $("#post_history_instructions_textarea").val(characters[chid].data?.post_history_instructions || '');
    $("#tags_textarea").val(Array.isArray(characters[chid].data?.tags) ? characters[chid].data.tags.join(', ') : '');
    $("#creator_textarea").val(characters[chid].data?.creator);
    $("#character_version_textarea").val(characters[chid].data?.character_version || '');
    $("#personality_textarea").val(characters[chid].personality);
    $("#firstmessage_textarea").val(characters[chid].first_mes);
    $("#scenario_pole").val(characters[chid].scenario);
    $("#depth_prompt_prompt").val(characters[chid].data?.extensions?.depth_prompt?.prompt ?? '');
    $("#depth_prompt_depth").val(characters[chid].data?.extensions?.depth_prompt?.depth ?? depth_prompt_depth_default);
    $("#talkativeness_slider").val(characters[chid].talkativeness || talkativeness_default);
    $("#mes_example_textarea").val(characters[chid].mes_example);
    $("#selected_chat_pole").val(characters[chid].chat);
    $("#create_date_pole").val(characters[chid].create_date);
    $("#avatar_url_pole").val(characters[chid].avatar);
    $("#chat_import_avatar_url").val(characters[chid].avatar);
    $("#chat_import_character_name").val(characters[chid].name);
    $("#character_json_data").val(characters[chid].json_data);
    let this_avatar = default_avatar;
    if (characters[chid].avatar != "none") {
        this_avatar = getThumbnailUrl('avatar', characters[chid].avatar);
    }

    updateFavButtonState(characters[chid].fav || characters[chid].fav == "true");

    $("#avatar_load_preview").attr("src", this_avatar);
    $("#name_div").removeClass('displayBlock');
    $("#name_div").addClass('displayNone');
    $("#renameCharButton").css("display", "");
    $('.open_alternate_greetings').data('chid', chid);
    $('#set_character_world').data('chid', chid);
    setWorldInfoButtonClass(chid);
    checkEmbeddedWorld(chid);

    $("#form_create").attr("actiontype", "editcharacter");
    $('.form_create_bottom_buttons_block .chat_lorebook_button').show();
    saveSettingsDebounced();
}

function select_rm_create() {
    menu_type = "create";

    //console.log('select_rm_Create() -- selected button: '+selected_button);
    if (selected_button == "create") {
        if (create_save.avatar != "") {
            $("#add_avatar_button").get(0).files = create_save.avatar;
            read_avatar_load($("#add_avatar_button").get(0));
        }
    }

    selectRightMenuWithAnimation('rm_ch_create_block');

    $('#set_chat_scenario').hide();
    $("#delete_button_div").css("display", "none");
    $("#delete_button").css("display", "none");
    $("#export_button").css("display", "none");
    $("#create_button_label").css("display", "");
    $("#create_button").attr("value", "Create");
    $("#dupe_button").hide();

    //create text poles
    $("#rm_button_back").css("display", "");
    $("#character_import_button").css("display", "");
    $("#character_popup_text_h3").text("Create character");
    $("#character_name_pole").val(create_save.name);
    $("#description_textarea").val(create_save.description);
    $('#character_world').val(create_save.world);
    $("#creator_notes_textarea").val(create_save.creator_notes);
    $("#creator_notes_spoiler").text(create_save.creator_notes);
    $("#post_history_instructions_textarea").val(create_save.post_history_instructions);
    $("#system_prompt_textarea").val(create_save.system_prompt);
    $("#tags_textarea").val(create_save.tags);
    $("#creator_textarea").val(create_save.creator);
    $("#character_version_textarea").val(create_save.character_version);
    $("#personality_textarea").val(create_save.personality);
    $("#firstmessage_textarea").val(create_save.first_message);
    $("#talkativeness_slider").val(create_save.talkativeness);
    $("#scenario_pole").val(create_save.scenario);
    $("#depth_prompt_prompt").val(create_save.depth_prompt_prompt);
    $("#depth_prompt_depth").val(create_save.depth_prompt_depth);
    $("#mes_example_textarea").val(create_save.mes_example.trim().length === 0 ? '<START>' : create_save.mes_example);
    $('#character_json_data').val('');
    $("#avatar_div").css("display", "flex");
    $("#avatar_load_preview").attr("src", default_avatar);
    $("#renameCharButton").css('display', 'none');
    $("#name_div").removeClass('displayNone');
    $("#name_div").addClass('displayBlock');
    $('.open_alternate_greetings').data('chid', undefined);
    $('#set_character_world').data('chid', undefined);
    setWorldInfoButtonClass(undefined, !!create_save.world);
    updateFavButtonState(false);
    checkEmbeddedWorld();

    $("#form_create").attr("actiontype", "createcharacter");
    $('.form_create_bottom_buttons_block .chat_lorebook_button').hide();
}

function select_rm_characters() {
    const doFullRefresh = menu_type === 'characters';
    menu_type = "characters";
    selectRightMenuWithAnimation('rm_characters_block');
    printCharacters(doFullRefresh);
}

/**
 * Sets a prompt injection to insert custom text into any outgoing prompt. For use in UI extensions.
 * @param {string} key Prompt injection id.
 * @param {string} value Prompt injection value.
 * @param {number} position Insertion position. 0 is after story string, 1 is in-chat with custom depth.
 * @param {number} depth Insertion depth. 0 represets the last message in context. Expected values up to MAX_INJECTION_DEPTH.
 */
export function setExtensionPrompt(key, value, position, depth) {
    extension_prompts[key] = { value: String(value), position: Number(position), depth: Number(depth) };
}

/**
 * Removes all char A/N prompt injections from the chat.
 * To clean up when switching from groups to solo and vice versa.
 */
export function removeDepthPrompts() {
    for (const key of Object.keys(extension_prompts)) {
        if (key.startsWith('DEPTH_PROMPT')) {
            delete extension_prompts[key];
        }
    }
}

/**
 * Adds or updates the metadata for the currently active chat.
 * @param {Object} newValues An object with collection of new values to be added into the metadata.
 * @param {boolean} reset Should a metadata be reset by this call.
 */
function updateChatMetadata(newValues, reset) {
    chat_metadata = reset ? { ...newValues } : { ...chat_metadata, ...newValues };
}

function updateFavButtonState(state) {
    fav_ch_checked = state;
    $("#fav_checkbox").val(fav_ch_checked);
    $("#favorite_button").toggleClass('fav_on', fav_ch_checked);
    $("#favorite_button").toggleClass('fav_off', !fav_ch_checked);
}

export function setScenarioOverride() {
    if (!selected_group && !this_chid) {
        console.warn('setScenarioOverride() -- no selected group or character');
        return;
    }

    const template = $('#scenario_override_template .scenario_override').clone();
    const metadataValue = chat_metadata['scenario'] || '';
    const isGroup = !!selected_group;
    template.find('[data-group="true"]').toggle(isGroup);
    template.find('[data-character="true"]').toggle(!isGroup);
    template.find('.chat_scenario').val(metadataValue).on('input', onScenarioOverrideInput);
    template.find('.remove_scenario_override').on('click', onScenarioOverrideRemoveClick);
    callPopup(template, 'text');
}

function onScenarioOverrideInput() {
    const value = String($(this).val());
    chat_metadata['scenario'] = value;
    saveMetadataDebounced();
}

function onScenarioOverrideRemoveClick() {
    $(this).closest('.scenario_override').find('.chat_scenario').val('').trigger('input');
}

function callPopup(text, type, inputValue = '', { okButton, rows, wide, large } = {}) {
    if (type) {
        popup_type = type;
    }

    if (wide) {
        $("#dialogue_popup").addClass("wide_dialogue_popup");
    }

    if (large) {
        $("#dialogue_popup").addClass("large_dialogue_popup");
    }

    $("#dialogue_popup_cancel").css("display", "inline-block");
    switch (popup_type) {
        case "avatarToCrop":
            $("#dialogue_popup_ok").text(okButton ?? "Accept");
            break;
        case "text":
        case "alternate_greeting":
        case "char_not_selected":
            $("#dialogue_popup_ok").text(okButton ?? "Ok");
            $("#dialogue_popup_cancel").css("display", "none");
            break;
        case "delete_extension":
            $("#dialogue_popup_ok").text(okButton ?? "Ok");
            break;
        case "new_chat":
        case "confirm":
            $("#dialogue_popup_ok").text(okButton ?? "Yes");
            break;
        case "del_group":
        case "rename_chat":
        case "del_chat":
        default:
            $("#dialogue_popup_ok").text(okButton ?? "Delete");
    }

    $("#dialogue_popup_input").val(inputValue);
    $("#dialogue_popup_input").attr("rows", rows ?? 1);

    if (popup_type == 'input') {
        $("#dialogue_popup_input").css("display", "block");
        $("#dialogue_popup_ok").text(okButton ?? "Save");
    }
    else {
        $("#dialogue_popup_input").css("display", "none");
    }

    $("#dialogue_popup_text").empty().append(text);
    $("#shadow_popup").css("display", "block");
    if (popup_type == 'input') {
        $("#dialogue_popup_input").focus();
    }
    if (popup_type == 'avatarToCrop') {
        // unset existing data
        crop_data = undefined;

        $('#avatarToCrop').cropper({
            aspectRatio: 2 / 3,
            autoCropArea: 1,
            viewMode: 2,
            rotatable: false,
            crop: function (event) {
                crop_data = event.detail;
                crop_data.want_resize = !power_user.never_resize_avatars
            }
        });
    }
    $("#shadow_popup").transition({
        opacity: 1,
        duration: 200,
        easing: animation_easing,
    });

    return new Promise((resolve) => {
        dialogueResolve = resolve;
    });
}

function showSwipeButtons() {
    if (chat.length === 0) {
        return;
    }

    if (
        chat[chat.length - 1].is_system ||
        !swipes ||
        $('.mes:last').attr('mesid') < 0 ||
        chat[chat.length - 1].is_user ||
        chat[chat.length - 1].extra?.image ||
        count_view_mes < 1 ||
        (selected_group && is_group_generating)
    ) { return; }

    // swipe_id should be set if alternate greetings are added
    if (chat.length == 1 && chat[0].swipe_id === undefined) {
        return;
    }

    //had to add this to make the swipe counter work
    //(copied from the onclick functions for swipe buttons..
    //don't know why the array isn't set for non-swipe messsages in Generate or addOneMessage..)

    if (chat[chat.length - 1]['swipe_id'] === undefined) {              // if there is no swipe-message in the last spot of the chat array
        chat[chat.length - 1]['swipe_id'] = 0;                        // set it to id 0
        chat[chat.length - 1]['swipes'] = [];                         // empty the array
        chat[chat.length - 1]['swipes'][0] = chat[chat.length - 1]['mes'];  //assign swipe array with last message from chat
    }

    const currentMessage = $("#chat").children().filter(`[mesid="${count_view_mes - 1}"]`);
    const swipeId = chat[chat.length - 1].swipe_id;
    var swipesCounterHTML = (`${(swipeId + 1)}/${(chat[chat.length - 1].swipes.length)}`);

    if (swipeId !== undefined && chat[chat.length - 1].swipes.length > 1) {
        currentMessage.children('.swipe_left').css('display', 'flex');
    }
    //only show right when generate is off, or when next right swipe would not make a generate happen
    if (is_send_press === false || chat[chat.length - 1].swipes.length >= swipeId) {
        currentMessage.children('.swipe_right').css('display', 'flex');
        currentMessage.children('.swipe_right').css('opacity', '0.3');
    }
    //console.log((chat[chat.length - 1]));
    if ((chat[chat.length - 1].swipes.length - swipeId) === 1) {
        //console.log('highlighting R swipe');
        currentMessage.children('.swipe_right').css('opacity', '0.7');
    }
    //console.log(swipesCounterHTML);

    $(".swipes-counter").html(swipesCounterHTML);

    //console.log(swipeId);
    //console.log(chat[chat.length - 1].swipes.length);
}

function hideSwipeButtons() {
    //console.log('hideswipebuttons entered');
    $("#chat").children().filter(`[mesid="${count_view_mes - 1}"]`).children('.swipe_right').css('display', 'none');
    $("#chat").children().filter(`[mesid="${count_view_mes - 1}"]`).children('.swipe_left').css('display', 'none');
}

export async function saveMetadata() {
    if (selected_group) {
        await editGroup(selected_group, true, false);
    }
    else {
        await saveChatConditional();
    }
}

export async function saveChatConditional() {
    try {
        await waitUntilCondition(() => !isChatSaving, durationSaveEdit, 100);
    } catch {
        console.warn('Timeout waiting for chat to save');
        return;
    }

    try {
        isChatSaving = true;

        if (selected_group) {
            await saveGroupChat(selected_group, true);
        }
        else {
            await saveChat();
        }

        // Save token cache to IndexedDB storage
        saveTokenCache();
    } catch (error) {
        console.error('Error saving chat', error);
    } finally {
        isChatSaving = false;
    }
}

async function importCharacterChat(formData) {
    await jQuery.ajax({
        type: "POST",
        url: "/importchat",
        data: formData,
        beforeSend: function () {
        },
        cache: false,
        contentType: false,
        processData: false,
        success: async function (data) {
            if (data.res) {
                await displayPastChats();
            }
        },
        error: function () {
            $("#create_button").removeAttr("disabled");
        },
    });
}

function updateViewMessageIds() {
    const minId = getFirstDisplayedMessageId();

    $('#chat').find(".mes").each(function (index, element) {
        $(element).attr("mesid", minId + index);
        $(element).find('.mesIDDisplay').text(`#${minId + index}`);
    });

    $('#chat .mes').removeClass('last_mes');
    $('#chat .mes').last().addClass('last_mes');

    updateEditArrowClasses();
}

function getFirstDisplayedMessageId() {
    const allIds = Array.from(document.querySelectorAll('#chat .mes')).map(el => Number(el.getAttribute('mesid'))).filter(x => !isNaN(x));
    const minId = Math.min(...allIds);
    return minId;
}

function updateEditArrowClasses() {
    $("#chat .mes .mes_edit_up").removeClass("disabled");
    $("#chat .mes .mes_edit_down").removeClass("disabled");

    if (this_edit_mes_id !== undefined) {
        const down = $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_down`);
        const up = $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_up`);
        const lastId = Number($("#chat .mes").last().attr("mesid"));
        const firstId = Number($("#chat .mes").first().attr("mesid"));

        if (lastId == Number(this_edit_mes_id)) {
            down.addClass("disabled");
        }

        if (firstId == Number(this_edit_mes_id)) {
            up.addClass("disabled");
        }
    }
}

function closeMessageEditor() {
    if (this_edit_mes_id) {
        $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click();
    }
}

function setGenerationProgress(progress) {
    if (!progress) {
        $('#send_textarea').css({ 'background': '', 'transition': '' });
    }
    else {
        $('#send_textarea').css({
            'background': `linear-gradient(90deg, #008000d6 ${progress}%, transparent ${progress}%)`,
            'transition': '0.25s ease-in-out'
        });
    }
}

function isHordeGenerationNotAllowed() {
    if (main_api == "koboldhorde" && preset_settings == "gui") {
        toastr.error('GUI Settings preset is not supported for Horde. Please select another preset.');
        return true;
    }

    return false;
}

export function cancelTtsPlay() {
    if ('speechSynthesis' in window) {
        speechSynthesis.cancel();
    }
}

async function deleteMessageImage() {
    const value = await callPopup("<h3>Delete image from message?<br>This action can't be undone.</h3>", 'confirm');

    if (!value) {
        return;
    }

    const mesBlock = $(this).closest('.mes');
    const mesId = mesBlock.attr('mesid');
    const message = chat[mesId];
    delete message.extra.image;
    delete message.extra.inline_image;
    mesBlock.find('.mes_img_container').removeClass('img_extra');
    mesBlock.find('.mes_img').attr('src', '');
    await saveChatConditional();
}

function enlargeMessageImage() {
    const mesBlock = $(this).closest('.mes');
    const mesId = mesBlock.attr('mesid');
    const message = chat[mesId];
    const imgSrc = message?.extra?.image;
    const title = message?.extra?.title;

    if (!imgSrc) {
        return;
    }

    const img = document.createElement('img');
    img.classList.add('img_enlarged');
    img.src = imgSrc;
    const imgContainer = $('<div><pre><code></code></pre></div>');
    imgContainer.prepend(img);
    imgContainer.addClass('img_enlarged_container');
    imgContainer.find('code').addClass('txt').text(title);
    addCopyToCodeBlocks(imgContainer);
    callPopup(imgContainer, 'text', '', { wide: true, large: true });
}

function updateAlternateGreetingsHintVisibility(root) {
    const numberOfGreetings = root.find('.alternate_greetings_list .alternate_greeting').length;
    $(root).find('.alternate_grettings_hint').toggle(numberOfGreetings == 0);
}

function openCharacterWorldPopup() {
    const chid = $('#set_character_world').data('chid');

    if (menu_type != 'create' && chid == undefined) {
        toastr.error('Does not have an Id for this character in world select menu.');
        return;
    }

    async function onSelectCharacterWorld() {
        const value = $('.character_world_info_selector').find('option:selected').val();
        const worldIndex = value !== '' ? Number(value) : NaN;
        const name = !isNaN(worldIndex) ? world_names[worldIndex] : '';

        const previousValue = $('#character_world').val();
        $('#character_world').val(name);

        console.debug('Character world selected:', name);

        if (menu_type == 'create') {
            create_save.world = name;
        } else {
            if (previousValue && !name) {
                try {
                    // Dirty hack to remove embedded lorebook from character JSON data.
                    const data = JSON.parse($('#character_json_data').val());

                    if (data?.data?.character_book) {
                        data.data.character_book = undefined;
                    }

                    $('#character_json_data').val(JSON.stringify(data));
                    toastr.info('Embedded lorebook will be removed from this character.');
                } catch {
                    console.error('Failed to parse character JSON data.');
                }
            }

            await createOrEditCharacter();
        }

        setWorldInfoButtonClass(undefined, !!value);
    }

    function onExtraWorldInfoChanged() {
        const selectedWorlds = $('.character_extra_world_info_selector').val();
        let charLore = world_info.charLore ?? [];

        // TODO: Maybe make this utility function not use the window context?
        const fileName = getCharaFilename(chid);
        const tempExtraBooks = selectedWorlds.map((index) => world_names[index]).filter((e) => e !== undefined);

        const existingCharIndex = charLore.findIndex((e) => e.name === fileName);
        if (existingCharIndex === -1) {
            const newCharLoreEntry = {
                name: fileName,
                extraBooks: tempExtraBooks
            }

            charLore.push(newCharLoreEntry);
        } else if (tempExtraBooks.length === 0) {
            charLore.splice(existingCharIndex, 1);
        } else {
            charLore[existingCharIndex].extraBooks = tempExtraBooks;
        }

        Object.assign(world_info, { charLore: charLore });
        saveSettingsDebounced();
    }

    const template = $('#character_world_template .character_world').clone();
    const select = template.find('.character_world_info_selector');
    const extraSelect = template.find('.character_extra_world_info_selector');
    const name = (menu_type == 'create' ? create_save.name : characters[chid]?.data?.name) || 'Nameless';
    const worldId = (menu_type == 'create' ? create_save.world : characters[chid]?.data?.extensions?.world) || '';
    template.find('.character_name').text(name);

    // Not needed on mobile
    const deviceInfo = getDeviceInfo();
    if (deviceInfo && deviceInfo.device.type === 'desktop') {
        $(extraSelect).select2({
            width: '100%',
            placeholder: 'No auxillary Lorebooks set. Click here to select.',
            allowClear: true,
            closeOnSelect: false,
        });
    }

    // Apped to base dropdown
    world_names.forEach((item, i) => {
        const option = document.createElement('option');
        option.value = i;
        option.innerText = item;
        option.selected = item === worldId;
        select.append(option);
    });

    // Append to extras dropdown
    if (world_names.length > 0) {
        extraSelect.empty();
    }
    world_names.forEach((item, i) => {
        const option = document.createElement('option');
        option.value = i;
        option.innerText = item;

        const existingCharLore = world_info.charLore?.find((e) => e.name === getCharaFilename());
        if (existingCharLore) {
            option.selected = existingCharLore.extraBooks.includes(item);
        } else {
            option.selected = false;
        }
        extraSelect.append(option);
    });

    select.on('change', onSelectCharacterWorld);
    extraSelect.on('mousedown change', async function (e) {
        // If there's no world names, don't do anything
        if (world_names.length === 0) {
            e.preventDefault();
            return;
        }

        onExtraWorldInfoChanged();
    });

    callPopup(template, 'text');
}

function openAlternateGreetings() {
    const chid = $('.open_alternate_greetings').data('chid');

    if (menu_type != 'create' && chid === undefined) {
        toastr.error('Does not have an Id for this character in editor menu.');
        return;
    } else {
        // If the character does not have alternate greetings, create an empty array
        if (chid && Array.isArray(characters[chid].data.alternate_greetings) == false) {
            characters[chid].data.alternate_greetings = [];
        }
    }

    const template = $('#alternate_greetings_template .alternate_grettings').clone();
    const getArray = () => menu_type == 'create' ? create_save.alternate_greetings : characters[chid].data.alternate_greetings;

    for (let index = 0; index < getArray().length; index++) {
        addAlternateGreeting(template, getArray()[index], index, getArray);
    }

    template.find('.add_alternate_greeting').on('click', function () {
        const array = getArray();
        const index = array.length;
        array.push(default_ch_mes);
        addAlternateGreeting(template, default_ch_mes, index, getArray);
        updateAlternateGreetingsHintVisibility(template);
    });

    updateAlternateGreetingsHintVisibility(template);
    callPopup(template, 'alternate_greeting');
}

function addAlternateGreeting(template, greeting, index, getArray) {
    const greetingBlock = $('#alternate_greeting_form_template .alternate_greeting').clone();
    greetingBlock.find('.alternate_greeting_text').on('input', async function () {
        const value = $(this).val();
        const array = getArray();
        array[index] = value;
    }).val(greeting);
    greetingBlock.find('.greeting_index').text(index + 1);
    greetingBlock.find('.delete_alternate_greeting').on('click', async function () {
        if (confirm('Are you sure you want to delete this alternate greeting?')) {
            const array = getArray();
            array.splice(index, 1);
            // We need to reopen the popup to update the index numbers
            openAlternateGreetings();
        }
    });
    template.find('.alternate_greetings_list').append(greetingBlock);
}

async function createOrEditCharacter(e) {
    $("#rm_info_avatar").html("");
    let save_name = create_save.name;
    var formData = new FormData($("#form_create").get(0));
    formData.set('fav', fav_ch_checked);
    if ($("#form_create").attr("actiontype") == "createcharacter") {
        if ($("#character_name_pole").val().length > 0) {
            //if the character name text area isn't empty (only posible when creating a new character)
            let url = "/createcharacter";

            if (crop_data != undefined) {
                url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`;
            }

            formData.delete('alternate_greetings');
            for (const value of create_save.alternate_greetings) {
                formData.append('alternate_greetings', value);
            }

            await jQuery.ajax({
                type: "POST",
                url: url,
                data: formData,
                beforeSend: function () {
                    $("#create_button").attr("disabled", true);
                    $("#create_button").attr("value", "⏳");
                },
                cache: false,
                contentType: false,
                processData: false,
                success: async function (html) {
                    $("#character_cross").trigger('click'); //closes the advanced character editing popup
                    const fields = [
                        { id: '#character_name_pole', callback: value => create_save.name = value },
                        { id: '#description_textarea', callback: value => create_save.description = value },
                        { id: '#creator_notes_textarea', callback: value => create_save.creator_notes = value },
                        { id: '#character_version_textarea', callback: value => create_save.character_version = value },
                        { id: '#post_history_instructions_textarea', callback: value => create_save.post_history_instructions = value },
                        { id: '#system_prompt_textarea', callback: value => create_save.system_prompt = value },
                        { id: '#tags_textarea', callback: value => create_save.tags = value },
                        { id: '#creator_textarea', callback: value => create_save.creator = value },
                        { id: '#personality_textarea', callback: value => create_save.personality = value },
                        { id: '#firstmessage_textarea', callback: value => create_save.first_message = value },
                        { id: '#talkativeness_slider', callback: value => create_save.talkativeness = value, defaultValue: talkativeness_default },
                        { id: '#scenario_pole', callback: value => create_save.scenario = value },
                        { id: '#depth_prompt_prompt', callback: value => create_save.depth_prompt_prompt = value },
                        { id: '#depth_prompt_depth', callback: value => create_save.depth_prompt_depth = value, defaultValue: depth_prompt_depth_default },
                        { id: '#mes_example_textarea', callback: value => create_save.mes_example = value },
                        { id: '#character_json_data', callback: () => { } },
                        { id: '#alternate_greetings_template', callback: value => create_save.alternate_greetings = value, defaultValue: [] },
                        { id: '#character_world', callback: value => create_save.world = value },
                    ];

                    fields.forEach(field => {
                        const fieldValue = field.defaultValue !== undefined ? field.defaultValue : '';
                        $(field.id).val(fieldValue);
                        field.callback && field.callback(fieldValue);
                    });

                    $("#character_popup_text_h3").text("Create character");

                    create_save.avatar = "";

                    $("#create_button").removeAttr("disabled");
                    $("#add_avatar_button").replaceWith(
                        $("#add_avatar_button").val("").clone(true)
                    );

                    $("#create_button").attr("value", "✅");
                    let oldSelectedChar = null;
                    if (this_chid != undefined && this_chid != "invalid-safety-id") {
                        oldSelectedChar = characters[this_chid].avatar;
                    }

                    console.log(`new avatar id: ${html}`);
                    createTagMapFromList("#tagList", html);
                    await getCharacters();

                    select_rm_info(`char_create`, html, oldSelectedChar);

                    crop_data = undefined;
                },
                error: function (jqXHR, exception) {
                    $("#create_button").removeAttr("disabled");
                },
            });
        } else {
            toastr.error('Name is required');
        }
    } else {
        let url = '/editcharacter';

        if (crop_data != undefined) {
            url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`;
        }

        formData.delete('alternate_greetings');
        const chid = $('.open_alternate_greetings').data('chid');
        if (chid && Array.isArray(characters[chid]?.data?.alternate_greetings)) {
            for (const value of characters[chid].data.alternate_greetings) {
                formData.append('alternate_greetings', value);
            }
        }

        await jQuery.ajax({
            type: "POST",
            url: url,
            data: formData,
            beforeSend: function () {
                $("#create_button").attr("disabled", true);
                $("#create_button").attr("value", "Save");
            },
            cache: false,
            contentType: false,
            processData: false,
            success: async function (html) {
                $("#create_button").removeAttr("disabled");

                await getOneCharacter(formData.get('avatar_url'));
                favsToHotswap(); // Update fav state

                $("#add_avatar_button").replaceWith(
                    $("#add_avatar_button").val("").clone(true)
                );
                $("#create_button").attr("value", "Save");
                crop_data = undefined;
                eventSource.emit(event_types.CHARACTER_EDITED, { detail: { id: this_chid, character: characters[this_chid] } });

                if (chat.length === 1 && !selected_group) {
                    const firstMessage = getFirstMessage();
                    chat[0] = firstMessage;

                    const chat_id = (chat.length - 1);
                    await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
                    clearChat();
                    await printMessages();
                    await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id);
                    await saveChatConditional();
                }
            },
            error: function (jqXHR, exception) {
                $("#create_button").removeAttr("disabled");
                console.log('Error! Either a file with the same name already existed, or the image file provided was in an invalid format. Double check that the image is not a webp.');
                toastr.error('Something went wrong while saving the character, or the image file provided was in an invalid format. Double check that the image is not a webp.');
            },
        });
    }
}

window["SillyTavern"].getContext = function () {
    return {
        chat: chat,
        characters: characters,
        groups: groups,
        name1: name1,
        name2: name2,
        characterId: this_chid,
        groupId: selected_group,
        chatId: selected_group
            ? groups.find(x => x.id == selected_group)?.chat_id
            : (this_chid && characters[this_chid] && characters[this_chid].chat),
        getCurrentChatId: getCurrentChatId,
        getRequestHeaders: getRequestHeaders,
        reloadCurrentChat: reloadCurrentChat,
        saveSettingsDebounced: saveSettingsDebounced,
        onlineStatus: online_status,
        maxContext: Number(max_context),
        chatMetadata: chat_metadata,
        streamingProcessor,
        eventSource: eventSource,
        event_types: event_types,
        addOneMessage: addOneMessage,
        generate: Generate,
        getTokenCount: getTokenCount,
        extensionPrompts: extension_prompts,
        setExtensionPrompt: setExtensionPrompt,
        updateChatMetadata: updateChatMetadata,
        saveChat: saveChatConditional,
        saveMetadata: saveMetadata,
        sendSystemMessage: sendSystemMessage,
        activateSendButtons,
        deactivateSendButtons,
        saveReply,
        registerSlashCommand: registerSlashCommand,
        registerHelper: registerExtensionHelper,
        registedDebugFunction: registerDebugFunction,
        renderExtensionTemplate: renderExtensionTemplate,
        callPopup: callPopup,
        mainApi: main_api,
        extensionSettings: extension_settings,
        ModuleWorkerWrapper: ModuleWorkerWrapper,
        getTokenizerModel: getTokenizerModel,
    };
};

function swipe_left() {      // when we swipe left..but no generation.
    if (chat.length - 1 === Number(this_edit_mes_id)) {
        closeMessageEditor();
    }

    if (isStreamingEnabled() && streamingProcessor) {
        streamingProcessor.isStopped = true;
    }

    const swipe_duration = 120;
    const swipe_range = '700px';
    chat[chat.length - 1]['swipe_id']--;

    if (chat[chat.length - 1]['swipe_id'] < 0) {
        chat[chat.length - 1]['swipe_id'] = chat[chat.length - 1]['swipes'].length - 1;
    }

    if (chat[chat.length - 1]['swipe_id'] >= 0) {
        /*$(this).parent().children('swipe_right').css('display', 'flex');
        if (chat[chat.length - 1]['swipe_id'] === 0) {
            $(this).css('display', 'none');
        }*/ // Just in case
        if (!Array.isArray(chat[chat.length - 1]['swipe_info'])) {
            chat[chat.length - 1]['swipe_info'] = [];
        }
        let this_mes_div = $(this).parent();
        let this_mes_block = $(this).parent().children('.mes_block').children('.mes_text');
        const this_mes_div_height = this_mes_div[0].scrollHeight;
        this_mes_div.css('height', this_mes_div_height);
        const this_mes_block_height = this_mes_block[0].scrollHeight;
        chat[chat.length - 1]['mes'] = chat[chat.length - 1]['swipes'][chat[chat.length - 1]['swipe_id']];
        chat[chat.length - 1]['send_date'] = chat[chat.length - 1].swipe_info[chat[chat.length - 1]['swipe_id']]?.send_date || chat[chat.length - 1].send_date; //load the last mes box with the latest generation
        chat[chat.length - 1]['extra'] = JSON.parse(JSON.stringify(chat[chat.length - 1].swipe_info[chat[chat.length - 1]['swipe_id']]?.extra || chat[chat.length - 1].extra));

        if (chat[chat.length - 1].extra) {
            // if message has memory attached - remove it to allow regen
            if (chat[chat.length - 1].extra.memory) {
                delete chat[chat.length - 1].extra.memory;
            }
            // ditto for display text
            if (chat[chat.length - 1].extra.display_text) {
                delete chat[chat.length - 1].extra.display_text;
            }
        }
        $(this).parent().children('.mes_block').transition({
            x: swipe_range,
            duration: swipe_duration,
            easing: animation_easing,
            queue: false,
            complete: function () {
                const is_animation_scroll = ($('#chat').scrollTop() >= ($('#chat').prop("scrollHeight") - $('#chat').outerHeight()) - 10);
                //console.log('on left swipe click calling addOneMessage');
                addOneMessage(chat[chat.length - 1], { type: 'swipe' });

                if (power_user.message_token_count_enabled) {
                    if (!chat[chat.length - 1].extra) {
                        chat[chat.length - 1].extra = {};
                    }

                    const swipeMessage = $("#chat").find(`[mesid="${count_view_mes - 1}"]`);
                    const tokenCount = getTokenCount(chat[chat.length - 1].mes, 0);
                    chat[chat.length - 1]['extra']['token_count'] = tokenCount;
                    swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
                }

                let new_height = this_mes_div_height - (this_mes_block_height - this_mes_block[0].scrollHeight);
                if (new_height < 103) new_height = 103;
                this_mes_div.animate({ height: new_height + 'px' }, {
                    duration: 0, //used to be 100
                    queue: false,
                    progress: function () {
                        // Scroll the chat down as the message expands

                        if (is_animation_scroll) $("#chat").scrollTop($("#chat")[0].scrollHeight);
                    },
                    complete: function () {
                        this_mes_div.css('height', 'auto');
                        // Scroll the chat down to the bottom once the animation is complete
                        if (is_animation_scroll) $("#chat").scrollTop($("#chat")[0].scrollHeight);
                    }
                });
                $(this).parent().children('.mes_block').transition({
                    x: '-' + swipe_range,
                    duration: 0,
                    easing: animation_easing,
                    queue: false,
                    complete: function () {
                        $(this).parent().children('.mes_block').transition({
                            x: '0px',
                            duration: swipe_duration,
                            easing: animation_easing,
                            queue: false,
                            complete: async function () {
                                await eventSource.emit(event_types.MESSAGE_SWIPED, (chat.length - 1));
                                saveChatDebounced();
                            }
                        });
                    }
                });
            }
        });

        $(this).parent().children('.avatar').transition({
            x: swipe_range,
            duration: swipe_duration,
            easing: animation_easing,
            queue: false,
            complete: function () {
                $(this).parent().children('.avatar').transition({
                    x: '-' + swipe_range,
                    duration: 0,
                    easing: animation_easing,
                    queue: false,
                    complete: function () {
                        $(this).parent().children('.avatar').transition({
                            x: '0px',
                            duration: swipe_duration,
                            easing: animation_easing,
                            queue: false,
                            complete: function () {

                            }
                        });
                    }
                });
            }
        });
    }
    if (chat[chat.length - 1]['swipe_id'] < 0) {
        chat[chat.length - 1]['swipe_id'] = 0;
    }
}

async function branchChat(mesID) {
    let name = await createBranch(mesID);
    await openCharacterChat(name);
}

// when we click swipe right button
const swipe_right = () => {
    if (chat.length - 1 === Number(this_edit_mes_id)) {
        closeMessageEditor();
    }

    if (isHordeGenerationNotAllowed()) {
        return;
    }

    const swipe_duration = 200;
    const swipe_range = 700;
    //console.log(swipe_range);
    let run_generate = false;
    let run_swipe_right = false;
    if (chat[chat.length - 1]['swipe_id'] === undefined) {              // if there is no swipe-message in the last spot of the chat array
        chat[chat.length - 1]['swipe_id'] = 0;                        // set it to id 0
        chat[chat.length - 1]['swipes'] = [];                         // empty the array
        chat[chat.length - 1]['swipe_info'] = [];
        chat[chat.length - 1]['swipes'][0] = chat[chat.length - 1]['mes'];  //assign swipe array with last message from chat
        chat[chat.length - 1]['swipe_info'][0] = { 'send_date': chat[chat.length - 1]['send_date'], 'gen_started': chat[chat.length - 1]['gen_started'], 'gen_finished': chat[chat.length - 1]['gen_finished'], 'extra': JSON.parse(JSON.stringify(chat[chat.length - 1]['extra'])) };
        //assign swipe info array with last message from chat
    }
    if (chat.length === 1 && chat[0]['swipe_id'] !== undefined && chat[0]['swipe_id'] === chat[0]['swipes'].length - 1) {    // if swipe_right is called on the last alternate greeting, loop back around
        chat[0]['swipe_id'] = 0;
    } else {
        chat[chat.length - 1]['swipe_id']++;                                // make new slot in array
    }
    if (chat[chat.length - 1].extra) {
        // if message has memory attached - remove it to allow regen
        if (chat[chat.length - 1].extra.memory) {
            delete chat[chat.length - 1].extra.memory;
        }
        // ditto for display text
        if (chat[chat.length - 1].extra.display_text) {
            delete chat[chat.length - 1].extra.display_text;
        }
    }
    if (!Array.isArray(chat[chat.length - 1]['swipe_info'])) {
        chat[chat.length - 1]['swipe_info'] = [];
    }
    //console.log(chat[chat.length-1]['swipes']);
    if (parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length && chat.length !== 1) { //if swipe id of last message is the same as the length of the 'swipes' array and not the greeting
        delete chat[chat.length - 1].gen_started;
        delete chat[chat.length - 1].gen_finished;
        run_generate = true;
    } else if (parseInt(chat[chat.length - 1]['swipe_id']) < chat[chat.length - 1]['swipes'].length) { //otherwise, if the id is less than the number of swipes
        chat[chat.length - 1]['mes'] = chat[chat.length - 1]['swipes'][chat[chat.length - 1]['swipe_id']]; //load the last mes box with the latest generation
        chat[chat.length - 1]['send_date'] = chat[chat.length - 1]?.swipe_info[chat[chat.length - 1]['swipe_id']]?.send_date || chat[chat.length - 1]['send_date']; //update send date
        chat[chat.length - 1]['extra'] = JSON.parse(JSON.stringify(chat[chat.length - 1].swipe_info[chat[chat.length - 1]['swipe_id']]?.extra || chat[chat.length - 1].extra || []));
        run_swipe_right = true; //then prepare to do normal right swipe to show next message
    }

    const currentMessage = $("#chat").children().filter(`[mesid="${count_view_mes - 1}"]`);
    let this_div = currentMessage.children('.swipe_right');
    let this_mes_div = this_div.parent();

    if (chat[chat.length - 1]['swipe_id'] > chat[chat.length - 1]['swipes'].length) { //if we swipe right while generating (the swipe ID is greater than what we are viewing now)
        chat[chat.length - 1]['swipe_id'] = chat[chat.length - 1]['swipes'].length; //show that message slot (will be '...' while generating)
    }
    if (run_generate) {               //hide swipe arrows while generating
        this_div.css('display', 'none');
    }
    // handles animated transitions when swipe right, specifically height transitions between messages
    if (run_generate || run_swipe_right) {
        let this_mes_block = this_mes_div.children('.mes_block').children('.mes_text');
        const this_mes_div_height = this_mes_div[0].scrollHeight;
        const this_mes_block_height = this_mes_block[0].scrollHeight;

        this_mes_div.children('.swipe_left').css('display', 'flex');
        this_mes_div.children('.mes_block').transition({        // this moves the div back and forth
            x: '-' + swipe_range,
            duration: swipe_duration,
            easing: animation_easing,
            queue: false,
            complete: function () {
                /*if (!selected_group) {
                    var typingIndicator = $("#typing_indicator_template .typing_indicator").clone();
                    typingIndicator.find(".typing_indicator_name").text(characters[this_chid].name);
                } */
                /* $("#chat").append(typingIndicator); */
                const is_animation_scroll = ($('#chat').scrollTop() >= ($('#chat').prop("scrollHeight") - $('#chat').outerHeight()) - 10);
                //console.log(parseInt(chat[chat.length-1]['swipe_id']));
                //console.log(chat[chat.length-1]['swipes'].length);
                const swipeMessage = $("#chat").find('[mesid="' + (count_view_mes - 1) + '"]');
                if (run_generate && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) {
                    //shows "..." while generating
                    swipeMessage.find('.mes_text').html('...');
                    // resets the timer
                    swipeMessage.find('.mes_timer').html('');
                    swipeMessage.find('.tokenCounterDisplay').text('');
                } else {
                    //console.log('showing previously generated swipe candidate, or "..."');
                    //console.log('onclick right swipe calling addOneMessage');
                    addOneMessage(chat[chat.length - 1], { type: 'swipe' });

                    if (power_user.message_token_count_enabled) {
                        if (!chat[chat.length - 1].extra) {
                            chat[chat.length - 1].extra = {};
                        }

                        const tokenCount = getTokenCount(chat[chat.length - 1].mes, 0);
                        chat[chat.length - 1]['extra']['token_count'] = tokenCount;
                        swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`);
                    }
                }
                let new_height = this_mes_div_height - (this_mes_block_height - this_mes_block[0].scrollHeight);
                if (new_height < 103) new_height = 103;


                this_mes_div.animate({ height: new_height + 'px' }, {
                    duration: 0, //used to be 100
                    queue: false,
                    progress: function () {
                        // Scroll the chat down as the message expands
                        if (is_animation_scroll) $("#chat").scrollTop($("#chat")[0].scrollHeight);
                    },
                    complete: function () {
                        this_mes_div.css('height', 'auto');
                        // Scroll the chat down to the bottom once the animation is complete
                        if (is_animation_scroll) $("#chat").scrollTop($("#chat")[0].scrollHeight);
                    }
                });
                this_mes_div.children('.mes_block').transition({
                    x: swipe_range,
                    duration: 0,
                    easing: animation_easing,
                    queue: false,
                    complete: function () {
                        this_mes_div.children('.mes_block').transition({
                            x: '0px',
                            duration: swipe_duration,
                            easing: animation_easing,
                            queue: false,
                            complete: async function () {
                                await eventSource.emit(event_types.MESSAGE_SWIPED, (chat.length - 1));
                                if (run_generate && !is_send_press && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) {
                                    console.debug('caught here 2');
                                    is_send_press = true;
                                    $('.mes_buttons:last').hide();
                                    await Generate('swipe');
                                } else {
                                    if (parseInt(chat[chat.length - 1]['swipe_id']) !== chat[chat.length - 1]['swipes'].length) {
                                        saveChatDebounced();
                                    }
                                }
                            }
                        });
                    }
                });
            }
        });
        this_mes_div.children('.avatar').transition({ // moves avatar along with swipe
            x: '-' + swipe_range,
            duration: swipe_duration,
            easing: animation_easing,
            queue: false,
            complete: function () {
                this_mes_div.children('.avatar').transition({
                    x: swipe_range,
                    duration: 0,
                    easing: animation_easing,
                    queue: false,
                    complete: function () {
                        this_mes_div.children('.avatar').transition({
                            x: '0px',
                            duration: swipe_duration,
                            easing: animation_easing,
                            queue: false,
                            complete: function () {

                            }
                        });
                    }
                });
            }
        });
    }
}



function displayOverrideWarnings() {
    if (!this_chid || !selected_group) {
        $('.prompt_overridden').hide();
        $('.jailbreak_overridden').hide();
        return;
    }

    $('.prompt_overridden').toggle(!!(characters[this_chid]?.data?.system_prompt));
    $('.jailbreak_overridden').toggle(!!(characters[this_chid]?.data?.post_history_instructions));
}

function connectAPISlash(_, text) {
    if (!text) return;

    const apiMap = {
        'kobold': {
            button: '#api_button',
        },
        'horde': {
            selected: 'koboldhorde',
        },
        'novel': {
            button: '#api_button_novel',
        },
        'ooba': {
            button: '#api_button_textgenerationwebui',
        },
        'oai': {
            selected: 'openai',
            source: 'openai',
            button: '#api_button_openai',
        },
        'claude': {
            selected: 'openai',
            source: 'claude',
            button: '#api_button_openai',
        },
        'windowai': {
            selected: 'openai',
            source: 'windowai',
            button: '#api_button_openai',
        },
        'openrouter': {
            selected: 'openai',
            source: 'openrouter',
            button: '#api_button_openai',
        },
        'scale': {
            selected: 'openai',
            source: 'scale',
            button: '#api_button_openai',
        },
        'ai21': {
            selected: 'openai',
            source: 'ai21',
            button: '#api_button_openai',
        },
        'palm': {
            selected: 'openai',
            source: 'palm',
            button: '#api_button_openai',
        },
    };

    const apiConfig = apiMap[text];
    if (!apiConfig) {
        toastr.error(`Error: ${text} is not a valid API`);
        return;
    }

    $(`#main_api option[value='${apiConfig.selected || text}']`).prop("selected", true);
    $("#main_api").trigger('change');

    if (apiConfig.source) {
        $(`#chat_completion_source option[value='${apiConfig.source}']`).prop("selected", true);
        $("#chat_completion_source").trigger('change');
    }

    if (apiConfig.button) {
        $(apiConfig.button).trigger('click');
    }

    toastr.info(`API set to ${text}, trying to connect..`);
}

export async function processDroppedFiles(files) {
    const allowedMimeTypes = [
        'application/json',
        'image/png',
    ];

    for (const file of files) {
        if (allowedMimeTypes.includes(file.type)) {
            await importCharacter(file);
        } else {
            toastr.warning('Unsupported file type: ' + file.name);
        }
    }
}

async function importCharacter(file) {
    const ext = file.name.match(/\.(\w+)$/);
    if (
        !ext ||
        (ext[1].toLowerCase() != "json" && ext[1].toLowerCase() != "png")
    ) {
        return;
    }

    const format = ext[1].toLowerCase();
    $("#character_import_file_type").val(format);
    const formData = new FormData();
    formData.append('avatar', file);
    formData.append('file_type', format);

    const data = await jQuery.ajax({
        type: "POST",
        url: "/importcharacter",
        data: formData,
        async: true,
        cache: false,
        contentType: false,
        processData: false,
    });

    if (data.error) {
        toastr.error('The file is likely invalid or corrupted.', 'Could not import character');
        return;
    }

    if (data.file_name !== undefined) {
        $('#character_search_bar').val('').trigger('input');

        let oldSelectedChar = null;
        if (this_chid != undefined && this_chid != "invalid-safety-id") {
            oldSelectedChar = characters[this_chid].avatar;
        }

        await getCharacters();
        select_rm_info(`char_import`, data.file_name, oldSelectedChar);
        if (power_user.import_card_tags) {
            let currentContext = getContext();
            let avatarFileName = `${data.file_name}.png`;
            let importedCharacter = currentContext.characters.find(character => character.avatar === avatarFileName);
            await importTags(importedCharacter);
        }
    }
}

async function importFromURL(items, files) {
    for (const item of items) {
        if (item.type === 'text/uri-list') {
            const uriList = await new Promise((resolve) => {
                item.getAsString((uriList) => { resolve(uriList); });
            });
            const uris = uriList.split('\n').filter(uri => uri.trim() !== '');
            try {
                for (const uri of uris) {
                    const request = await fetch(uri);
                    const data = await request.blob();
                    const fileName = request.headers.get('Content-Disposition')?.split('filename=')[1]?.replace(/"/g, '') || uri.split('/').pop() || 'file.png';
                    const file = new File([data], fileName, { type: data.type });
                    files.push(file);
                }
            } catch (error) {
                console.error('Failed to import from URL', error);
            }
        }
    }
}

async function doImpersonate() {
    $('#send_textarea').val('');
    $("#option_impersonate").trigger('click', { fromSlashCommand: true })
}

async function doDeleteChat() {
    $("#option_select_chat").trigger('click', { fromSlashCommand: true })
    await delay(100)
    let currentChatDeleteButton = $(".select_chat_block[highlight='true']").parent().find('.PastChat_cross')
    $(currentChatDeleteButton).trigger('click', { fromSlashCommand: true })
    await delay(1)
    $("#dialogue_popup_ok").trigger('click')
    //200 delay needed let the past chat view reshow first
    await delay(200)
    $("#select_chat_cross").trigger('click')
}

const isPwaMode = window.navigator.standalone;
if (isPwaMode) { $("body").addClass('PWA') }

function doCharListDisplaySwitch() {
    console.debug('toggling body charListGrid state')
    $("body").toggleClass('charListGrid')
    power_user.charListGrid = $("body").hasClass("charListGrid") ? true : false;
    saveSettingsDebounced();
}

function doCloseChat() {
    $("#option_close_chat").trigger('click')
}

/**
 * Function to handle the deletion of a character, given a specific popup type and character ID.
 * If popup type equals "del_ch", it will proceed with deletion otherwise it will exit the function.
 * It fetches the delete character route, sending necessary parameters, and in case of success,
 * it proceeds to delete character from UI and saves settings.
 * In case of error during the fetch request, it logs the error details.
 *
 * @param {string} popup_type - The type of popup currently active.
 * @param {string} this_chid - The character ID to be deleted.
 * @param {boolean} delete_chats - Whether to delete chats or not.
 */
export async function handleDeleteCharacter(popup_type, this_chid, delete_chats) {
    if (popup_type !== "del_ch") {
        return;
    }

    const avatar = characters[this_chid].avatar;
    const name = characters[this_chid].name;
    const pastChats = await getPastCharacterChats();

    const msg = { avatar_url: avatar, delete_chats: delete_chats };

    const response = await fetch('/deletecharacter', {
        method: 'POST',
        headers: getRequestHeaders(),
        body: JSON.stringify(msg),
        cache: 'no-cache',
    });

    if (response.ok) {
        await deleteCharacter(name, avatar);

        if (delete_chats) {
            for (const chat of pastChats) {
                const name = chat.file_name.replace('.jsonl', '');
                await eventSource.emit(event_types.CHAT_DELETED, name);
            }
        }
    } else {
        console.error('Failed to delete character: ', response.status, response.statusText);
    }
}

/**
 * Function to delete a character from UI after character deletion API success.
 * It manages necessary UI changes such as closing advanced editing popup, unsetting
 * character ID, resetting characters array and chat metadata, deselecting character's tab
 * panel, removing character name from navigation tabs, clearing chat, removing character's
 * avatar from tag_map, fetching updated list of characters and updating the 'deleted
 * character' message.
 * It also ensures to save the settings after all the operations.
 *
 * @param {string} name - The name of the character to be deleted.
 * @param {string} avatar - The avatar URL of the character to be deleted.
 */
export async function deleteCharacter(name, avatar) {
    $("#character_cross").click();
    this_chid = "invalid-safety-id";
    characters.length = 0;
    name2 = systemUserName;
    chat = [...safetychat];
    chat_metadata = {};
    $(document.getElementById("rm_button_selected_ch")).children("h2").text("");
    clearChat();
    this_chid = undefined;
    delete tag_map[avatar];
    await getCharacters();
    select_rm_info("char_delete", name);
    await printMessages();
    saveSettingsDebounced();
}

function doTogglePanels() {
    $("#option_settings").trigger('click')
}

jQuery(async function () {

    if (isMobile() === true) {
        console.debug('hiding movingUI and sheldWidth toggles for mobile')
        $("#sheldWidthToggleBlock").hide();
        $("#movingUIModeCheckBlock").hide();

    }

    registerSlashCommand('dupe', DupeChar, [], "– duplicates the currently selected character", true, true);
    registerSlashCommand('api', connectAPISlash, [], '<span class="monospace">(kobold, horde, novel, ooba, oai, claude, windowai, openrouter, scale, ai21, palm)</span> – connect to an API', true, true);
    registerSlashCommand('impersonate', doImpersonate, ['imp'], "– calls an impersonation response", true, true);
    registerSlashCommand('delchat', doDeleteChat, [], "– deletes the current chat", true, true);
    registerSlashCommand('closechat', doCloseChat, [], "– closes the current chat", true, true);
    registerSlashCommand('panels', doTogglePanels, ['togglepanels'], "– toggle UI panels on/off", true, true);

    setTimeout(function () {
        $("#groupControlsToggle").trigger('click');
        $("#groupCurrentMemberListToggle .inline-drawer-icon").trigger('click');
    }, 200);

    $("#chat").on('mousewheel touchstart', () => {
        scrollLock = true;
    });

    //////////INPUT BAR FOCUS-KEEPING LOGIC/////////////
    let S_TAFocused = false;
    let S_TAPreviouslyFocused = false;
    $('#send_textarea').on('focusin focus click', () => {
        S_TAFocused = true;
        S_TAPreviouslyFocused = true;
    });
    $('#send_textarea').on('focusout blur', () => S_TAFocused = false);
    $('#options_button, #send_but, #option_regenerate, #option_continue, #mes_continue').on('click', () => {
        if (S_TAPreviouslyFocused) {
            $('#send_textarea').focus();
            S_TAFocused = true;
        }
    });
    $(document).click(event => {
        if ($(':focus').attr('id') !== 'send_textarea') {
            var validIDs = ["options_button", "send_but", "mes_continue", "send_textarea", "option_regenerate", "option_continue"];
            if (!validIDs.includes($(event.target).attr('id'))) {
                S_TAFocused = false;
                S_TAPreviouslyFocused = false;
            }
        } else {
            S_TAFocused = true;
            S_TAPreviouslyFocused = true;
        }
    });

    /////////////////

    $('#swipes-checkbox').change(function () {
        swipes = !!$('#swipes-checkbox').prop('checked');
        if (swipes) {
            //console.log('toggle change calling showswipebtns');
            showSwipeButtons();
        } else {
            hideSwipeButtons();
        }
        saveSettingsDebounced();
    });

    ///// SWIPE BUTTON CLICKS ///////

    $(document).on('click', '.swipe_right', swipe_right);

    $(document).on('click', '.swipe_left', swipe_left);

    $("#character_search_bar").on("input", function () {
        const searchValue = String($(this).val()).toLowerCase();
        entitiesFilter.setFilterData(FILTER_TYPES.SEARCH, searchValue);
    });

    $("#mes_continue").on('click', function () {
        $("#option_continue").trigger('click');
    });

    $("#send_but").on('click', function () {
        if (is_send_press == false) {
            is_send_press = true;
            Generate();
        }
    });

    //menu buttons setup

    $("#rm_button_settings").click(function () {
        selected_button = "settings";
        menu_type = "settings";
        selectRightMenuWithAnimation('rm_api_block');
    });
    $("#rm_button_characters").click(function () {
        selected_button = "characters";
        select_rm_characters();
    });
    $("#rm_button_back").click(function () {
        selected_button = "characters";
        select_rm_characters();
    });
    $("#rm_button_create").click(function () {
        selected_button = "create";
        select_rm_create();
    });
    $("#rm_button_selected_ch").click(function () {
        if (selected_group) {
            select_group_chats(selected_group);
        } else {
            selected_button = "character_edit";
            select_selected_character(this_chid);
        }
        $("#character_search_bar").val("").trigger("input");
    });

    $(document).on("click", ".character_select", function () {
        const id = $(this).attr("chid");
        selectCharacterById(id);
    });

    $(document).on("input", ".edit_textarea", function () {
        scroll_holder = $("#chat").scrollTop();
        $(this).height(0).height(this.scrollHeight);
        is_use_scroll_holder = true;
    });
    $("#chat").on("scroll", function () {
        if (is_use_scroll_holder) {
            $("#chat").scrollTop(scroll_holder);
            is_use_scroll_holder = false;
        }
    });

    $(document).on("click", ".mes", function () {
        //when a 'delete message' parent div is clicked
        // and we are in delete mode and del_checkbox is visible
        if (!is_delete_mode || !$(this).children('.del_checkbox').is(':visible')) {
            return;
        }
        $(".mes").children(".del_checkbox").each(function () {
            $(this).prop("checked", false);
            $(this).parent().css("background", css_mes_bg);
        });
        $(this).css("background", "#600"); //sets the bg of the mes selected for deletion
        var i = Number($(this).attr("mesid")); //checks the message ID in the chat
        this_del_mes = i;
        while (i < chat.length) {
            //as long as the current message ID is less than the total chat length
            $(".mes[mesid='" + i + "']").css("background", "#600"); //sets the bg of the all msgs BELOW the selected .mes
            $(".mes[mesid='" + i + "']")
                .children(".del_checkbox")
                .prop("checked", true);
            i++;
            //console.log(i);
        }
    });

    $(document).on("click", "#user_avatar_block .avatar", setUserAvatar);
    $(document).on("click", "#user_avatar_block .avatar_upload", function () {
        $("#avatar_upload_overwrite").val("");
        $("#avatar_upload_file").trigger('click');
    });
    $(document).on("click", "#user_avatar_block .set_persona_image", function () {
        const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile');

        if (!avatarId) {
            console.log('no imgfile');
            return;
        }

        $("#avatar_upload_overwrite").val(avatarId);
        $("#avatar_upload_file").trigger('click');
    });
    $("#avatar_upload_file").on("change", uploadUserAvatar);

    $(document).on("click", ".PastChat_cross", function () {
        chat_file_for_del = $(this).attr('file_name');
        console.debug('detected cross click for' + chat_file_for_del);
        popup_type = "del_chat";
        callPopup("<h3>Delete the Chat File?</h3>");
    });

    $("#advanced_div").click(function () {
        if (!is_advanced_char_open) {
            is_advanced_char_open = true;
            $("#character_popup").css("display", "flex");
            $("#character_popup").css("opacity", 0.0);
            $("#character_popup").transition({
                opacity: 1.0,
                duration: animation_duration,
                easing: animation_easing,
            });
        } else {
            is_advanced_char_open = false;
            $("#character_popup").css("display", "none");
        }
    });

    $("#character_cross").click(function () {
        is_advanced_char_open = false;
        $("#character_popup").transition({
            opacity: 0,
            duration: 200,
            easing: animation_easing,
        });
        setTimeout(function () { $("#character_popup").css("display", "none"); }, 200);
    });

    $("#character_popup_ok").click(function () {
        is_advanced_char_open = false;
        $("#character_popup").css("display", "none");
    });

    $("#dialogue_popup_ok").click(async function (e) {
        $("#shadow_popup").transition({
            opacity: 0,
            duration: 200,
            easing: animation_easing,
        });
        setTimeout(function () {
            $("#shadow_popup").css("display", "none");
            $("#dialogue_popup").removeClass('large_dialogue_popup');
            $("#dialogue_popup").removeClass('wide_dialogue_popup');
        }, 200);

        //      $("#shadow_popup").css("opacity:", 0.0);

        if (popup_type == 'avatarToCrop') {
            dialogueResolve($("#avatarToCrop").data('cropper').getCroppedCanvas().toDataURL('image/jpeg'));
        };

        if (popup_type == "del_chat") {
            //close past chat popup
            $("#select_chat_cross").click();

            if (selected_group) {
                await deleteGroupChat(selected_group, chat_file_for_del);
            } else {
                await delChat(chat_file_for_del);
            }

            //open the history view again after 100ms
            //hide option popup menu
            setTimeout(function () {
                $("#option_select_chat").click();
                $("#options").hide();
            }, 2000);
        }
        if (popup_type == "del_ch") {
            const deleteChats = !!$("#del_char_checkbox").prop("checked");
            await handleDeleteCharacter(popup_type, this_chid, deleteChats);
            eventSource.emit('characterDeleted', { id: this_chid, character: characters[this_chid] });
        }
        if (popup_type == "alternate_greeting" && menu_type !== "create") {
            createOrEditCharacter();
        }
        if (popup_type === "del_group") {
            const groupId = $("#dialogue_popup").data("group_id");

            if (groupId) {
                deleteGroup(groupId);
            }
        }
        //Make a new chat for selected character
        if (
            popup_type == "new_chat" &&
            (selected_group || this_chid !== undefined) &&
            menu_type != "create"
        ) {
            //Fix it; New chat doesn't create while open create character menu
            clearChat();
            chat.length = 0;

            if (selected_group) {
                await createNewGroupChat(selected_group);
            }
            else {
                //RossAscends: added character name to new chat filenames and replaced Date.now() with humanizedDateTime;
                chat_metadata = {};
                characters[this_chid].chat = name2 + " - " + humanizedDateTime();
                $("#selected_chat_pole").val(characters[this_chid].chat);
                await getChat();
                await createOrEditCharacter();
            }
        }

        rawPromptPopper.update();
        $('#rawPromptPopup').hide();

        if (dialogueResolve) {
            if (popup_type == 'input') {
                dialogueResolve($("#dialogue_popup_input").val());
                $("#dialogue_popup_input").val('');

            }
            else {
                dialogueResolve(true);

            }

            dialogueResolve = null;
        }
    });

    $("#dialogue_popup_cancel").click(function (e) {
        $("#shadow_popup").transition({
            opacity: 0,
            duration: 200,
            easing: animation_easing,
        });
        setTimeout(function () {
            $("#shadow_popup").css("display", "none");
            $("#dialogue_popup").removeClass('large_dialogue_popup');
        }, 200);

        //$("#shadow_popup").css("opacity:", 0.0);
        popup_type = "";

        if (dialogueResolve) {
            dialogueResolve(false);
            dialogueResolve = null;
        }

    });

    $("#add_avatar_button").change(function () {
        read_avatar_load(this);
    });

    $("#form_create").submit(createOrEditCharacter);

    $("#delete_button").on('click', function () {
        popup_type = "del_ch";
        callPopup(`
                <h3>Delete the character?</h3>
                <b>THIS IS PERMANENT!<br><br>
                <label for="del_char_checkbox" class="checkbox_label justifyCenter">
                    <input type="checkbox" id="del_char_checkbox" />
                    <span>Also delete the chat files</span>
                </label><br></b>`
        );
    });

    //////// OPTIMIZED ALL CHAR CREATION/EDITING TEXTAREA LISTENERS ///////////////

    $("#character_name_pole").on("input", function () {
        if (menu_type == "create") {
            create_save.name = String($("#character_name_pole").val());
        }
    });

    const elementsToUpdate = {
        '#description_textarea': function () { create_save.description = String($("#description_textarea").val()); },
        '#creator_notes_textarea': function () { create_save.creator_notes = String($("#creator_notes_textarea").val()); },
        '#character_version_textarea': function () { create_save.character_version = String($("#character_version_textarea").val()); },
        '#system_prompt_textarea': function () { create_save.system_prompt = String($("#system_prompt_textarea").val()); },
        '#post_history_instructions_textarea': function () { create_save.post_history_instructions = String($("#post_history_instructions_textarea").val()); },
        '#creator_textarea': function () { create_save.creator = String($("#creator_textarea").val()); },
        '#tags_textarea': function () { create_save.tags = String($("#tags_textarea").val()); },
        '#personality_textarea': function () { create_save.personality = String($("#personality_textarea").val()); },
        '#scenario_pole': function () { create_save.scenario = String($("#scenario_pole").val()); },
        '#mes_example_textarea': function () { create_save.mes_example = String($("#mes_example_textarea").val()); },
        '#firstmessage_textarea': function () { create_save.first_message = String($("#firstmessage_textarea").val()); },
        '#talkativeness_slider': function () { create_save.talkativeness = Number($("#talkativeness_slider").val()); },
        '#depth_prompt_prompt': function () { create_save.depth_prompt_prompt = String($("#depth_prompt_prompt").val()); },
        '#depth_prompt_depth': function () { create_save.depth_prompt_depth = Number($("#depth_prompt_depth").val()); },
    };

    Object.keys(elementsToUpdate).forEach(function (id) {
        $(id).on("input", function () {
            if (menu_type == "create") {
                elementsToUpdate[id]();
            } else {
                saveCharacterDebounced();
            }
        });
    });

    $("#favorite_button").on('click', function () {
        updateFavButtonState(!fav_ch_checked);
        if (menu_type != "create") {
            saveCharacterDebounced();
        }
    });

    /* $("#renameCharButton").on('click', renameCharacter); */

    $(document).on("click", ".renameChatButton", async function () {
        const old_filenamefull = $(this).closest('.select_chat_block_wrapper').find('.select_chat_block_filename').text();
        const old_filename = old_filenamefull.replace('.jsonl', '');

        const popupText = `<h3>Enter the new name for the chat:<h3>
        <small>!!Using an existing filename will produce an error!!<br>
        This will break the link between bookmark chats.<br>
        No need to add '.jsonl' at the end.<br>
        </small>`;
        const newName = await callPopup(popupText, 'input', old_filename);

        if (!newName || newName == old_filename) {
            console.log('no new name found, aborting');
            return;
        }

        const body = {
            is_group: !!selected_group,
            avatar_url: characters[this_chid]?.avatar,
            original_file: `${old_filename}.jsonl`,
            renamed_file: `${newName}.jsonl`,
        }

        try {
            const response = await fetch('/renamechat', {
                method: 'POST',
                body: JSON.stringify(body),
                headers: getRequestHeaders(),
            });

            if (!response.ok) {
                throw new Error('Unsuccessful request.');
            }

            const data = response.json();

            if (data.error) {
                throw new Error('Server returned an error.');
            }

            if (selected_group) {
                await renameGroupChat(selected_group, old_filename, newName);
            }
            else {
                if (characters[this_chid].chat == old_filename) {
                    characters[this_chid].chat = newName;
                    await createOrEditCharacter();
                }
            }

            await reloadCurrentChat();

            await delay(250);
            $("#option_select_chat").trigger('click');
            $("#options").hide();
        } catch {
            await delay(500);
            await callPopup('An error has occurred. Chat was not renamed.', 'text');
        }
    });

    $(document).on("click", ".exportChatButton, .exportRawChatButton", async function () {
        const format = $(this).data('format') || 'txt';
        await saveChatConditional();
        const filenamefull = $(this).closest('.select_chat_block_wrapper').find('.select_chat_block_filename').text();
        console.log(`exporting ${filenamefull} in ${format} format`);

        const filename = filenamefull.replace('.jsonl', '');
        const body = {
            is_group: !!selected_group,
            avatar_url: characters[this_chid]?.avatar,
            file: `${filename}.jsonl`,
            exportfilename: `${filename}.${format}`,
            format: format,
        }
        console.log(body);
        try {
            const response = await fetch('/exportchat', {
                method: 'POST',
                body: JSON.stringify(body),
                headers: getRequestHeaders(),
            });
            const data = await response.json();
            if (!response.ok) {
                // display error message
                console.log(data.message);
                await delay(250);
                toastr.error(`Error: ${data.message}`);
                return;
            } else {
                const mimeType = format == 'txt' ? 'text/plain' : 'application/octet-stream';
                // success, handle response data
                console.log(data);
                await delay(250);
                toastr.success(data.message);
                download(data.result, body.exportfilename, mimeType);
            }
        } catch (error) {
            // display error message
            console.log(`An error has occurred: ${error.message}`);
            await delay(250);
            toastr.error(`Error: ${error.message}`);
        }
    });

    ///////////////////////////////////////////////////////////////////////////////////

    $("#api_button").click(function (e) {
        if ($("#api_url_text").val() != "") {
            let value = formatKoboldUrl(String($("#api_url_text").val()).trim());

            if (!value) {
                toastr.error('Please enter a valid URL.');
                return;
            }

            $("#api_url_text").val(value);
            api_server = value;
            $("#api_loading").css("display", "inline-block");
            $("#api_button").css("display", "none");

            main_api = "kobold";
            saveSettingsDebounced();
            is_get_status = true;
            is_api_button_press = true;
            getStatus();
        }
    });

    $("#api_button_textgenerationwebui").on('click', async function (e) {
        const urlSourceId = getTextGenUrlSourceId();

        if ($(urlSourceId).val() != "") {
            let value = formatTextGenURL(String($(urlSourceId).val()).trim(), isMancer());
            if (!value) {
                callPopup("Please enter a valid URL.<br/>WebUI URLs should end with <tt>/api</tt><br/>Enable 'Relaxed API URLs' to allow other paths.", 'text');
                return;
            }

            const mancerKey = String($("#api_key_mancer").val()).trim();
            if (mancerKey.length) {
                await writeSecret(SECRET_KEYS.MANCER, mancerKey);
            }

            const aphroditeKey = String($("#api_key_aphrodite").val()).trim();
            if (aphroditeKey.length) {
                await writeSecret(SECRET_KEYS.APHRODITE, aphroditeKey);
            }

            $(urlSourceId).val(value);
            $("#api_loading_textgenerationwebui").css("display", "inline-block");
            $("#api_button_textgenerationwebui").css("display", "none");

            api_server_textgenerationwebui = value;
            main_api = "textgenerationwebui";
            saveSettingsDebounced();
            is_get_status = true;
            is_api_button_press = true;
            getStatus();
        }
    });

    var button = $('#options_button');
    var menu = $('#options');

    function showMenu() {
        showBookmarksButtons();
        menu.stop().fadeIn(250);
        optionsPopper.update();
    }

    function hideMenu() {
        menu.stop().fadeOut(250);
        optionsPopper.update();
    }

    function isMouseOverButtonOrMenu() {
        return menu.is(':hover') || button.is(':hover');
    }

    button.on('mouseenter click', function () { showMenu(); });
    button.on('mouseleave', function () {
        //delay to prevent menu hiding when mouse leaves button into menu
        setTimeout(() => {
            if (!isMouseOverButtonOrMenu()) { hideMenu(); }
        }, 100)
    });
    menu.on('mouseleave', function () {
        //delay to prevent menu hide when mouseleaves menu into button
        setTimeout(() => {
            if (!isMouseOverButtonOrMenu()) { hideMenu(); }
        }, 100)
    });
    $(document).on('click', function () {
        if (!isMouseOverButtonOrMenu() && menu.is(':visible')) { hideMenu(); }
    });

    /* $('#set_chat_scenario').on('click', setScenarioOverride); */

    ///////////// OPTIMIZED LISTENERS FOR LEFT SIDE OPTIONS POPUP MENU //////////////////////
    $("#options [id]").on("click", function (event, customData) {
        const fromSlashCommand = customData?.fromSlashCommand || false;
        var id = $(this).attr("id");

        if (id == "option_select_chat") {
            if ((selected_group && !is_group_generating) || (this_chid !== undefined && !is_send_press) || fromSlashCommand) {
                displayPastChats();
                //this is just to avoid the shadow for past chat view when using /delchat
                //however, the dialog popup still gets one..
                if (!fromSlashCommand) {
                    console.log('displaying shadow')
                    $("#shadow_select_chat_popup").css("display", "block");
                    $("#shadow_select_chat_popup").css("opacity", 0.0);
                    $("#shadow_select_chat_popup").transition({
                        opacity: 1.0,
                        duration: animation_duration,
                        easing: animation_easing,
                    });
                }
            }
        }

        else if (id == "option_start_new_chat") {
            if ((selected_group || this_chid !== undefined) && !is_send_press) {
                popup_type = "new_chat";
                callPopup("<h3>Start new chat?</h3>");
            }
        }

        else if (id == "option_regenerate") {
            closeMessageEditor();
            if (is_send_press == false) {
                //hideSwipeButtons();

                if (selected_group) {
                    regenerateGroup();
                }
                else {
                    is_send_press = true;
                    Generate("regenerate");
                }
            }
        }

        else if (id == "option_impersonate") {
            if (is_send_press == false || fromSlashCommand) {
                is_send_press = true;
                Generate("impersonate");
            }
        }

        else if (id == 'option_continue') {
            if (is_send_press == false || fromSlashCommand) {
                is_send_press = true;
                Generate("continue");
            }
        }

        else if (id == "option_delete_mes") {
            setTimeout(openMessageDelete, animation_duration);
        }

        else if (id == "option_close_chat") {
            if (is_send_press == false) {
                clearChat();
                chat.length = 0;
                resetSelectedGroup();
                setCharacterId(undefined);
                setCharacterName('');
                setActiveCharacter(null);
                setActiveGroup(null);
                this_edit_mes_id = undefined;
                chat_metadata = {};
                selected_button = "characters";
                $("#rm_button_selected_ch").children("h2").text('');
                select_rm_characters();
                sendSystemMessage(system_message_types.WELCOME);
                eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId());
            } else {
                toastr.info("Please stop the message generation first.");
            }
        }

        else if (id === "option_settings") {
            //var checkBox = document.getElementById("waifuMode");
            var topBar = document.getElementById("top-bar");
            var topSettingsHolder = document.getElementById("top-settings-holder");
            var divchat = document.getElementById("chat");

            //if (checkBox.checked) {
            if (topBar.style.display === "none") {
                topBar.style.display = ""; // or "inline-block" if that's the original display value
                topSettingsHolder.style.display = ""; // or "inline-block" if that's the original display value

                divchat.style.borderRadius = "";
                divchat.style.backgroundColor = "";

            } else {

                divchat.style.borderRadius = "10px"; // Adjust the value to control the roundness of the corners
                divchat.style.backgroundColor = ""; // Set the background color to your preference

                topBar.style.display = "none";
                topSettingsHolder.style.display = "none";
            }
            //}
        }
        hideMenu();
    });

    //////////////////////////////////////////////////////////////////////////////////////////////

    //functionality for the cancel delete messages button, reverts to normal display of input form
    $("#dialogue_del_mes_cancel").click(function () {
        $("#dialogue_del_mes").css("display", "none");
        $("#send_form").css("display", css_send_form_display);
        $(".del_checkbox").each(function () {
            $(this).css("display", "none");
            $(this).parent().children(".for_checkbox").css("display", "block");
            $(this).parent().css("background", css_mes_bg);
            $(this).prop("checked", false);
        });
        this_del_mes = 0;
        console.debug('canceled del msgs, calling showswipesbtns');
        showSwipeButtons();
        is_delete_mode = false;
    });

    //confirms message deletion with the "ok" button
    $("#dialogue_del_mes_ok").click(async function () {
        $("#dialogue_del_mes").css("display", "none");
        $("#send_form").css("display", css_send_form_display);
        $(".del_checkbox").each(function () {
            $(this).css("display", "none");
            $(this).parent().children(".for_checkbox").css("display", "block");
            $(this).parent().css("background", css_mes_bg);
            $(this).prop("checked", false);
        });
        if (this_del_mes != 0) {
            $(".mes[mesid='" + this_del_mes + "']")
                .nextAll("div")
                .remove();
            $(".mes[mesid='" + this_del_mes + "']").remove();
            chat.length = this_del_mes;
            count_view_mes = this_del_mes;
            await saveChatConditional();
            var $textchat = $("#chat");
            $textchat.scrollTop($textchat[0].scrollHeight);
            eventSource.emit(event_types.MESSAGE_DELETED, chat.length);
        }
        this_del_mes = 0;
        $('#chat .mes').last().addClass('last_mes');
        $('#chat .mes').eq(-2).removeClass('last_mes');
        console.debug('confirmed del msgs, calling showswipesbtns');
        showSwipeButtons();
        is_delete_mode = false;
    });

    $("#settings_perset").change(function () {
        if ($("#settings_perset").find(":selected").val() != "gui") {
            preset_settings = $("#settings_perset").find(":selected").text();
            const preset = koboldai_settings[koboldai_setting_names[preset_settings]];
            loadKoboldSettings(preset);

            setGenerationParamsFromPreset(preset);

            $("#range_block").find('input').prop("disabled", false);
            $("#range_block").css("opacity", 1.0);

            $("#kobold-advanced-config").find('input').prop("disabled", false);
            $("#kobold-advanced-config").css('opacity', 1.0);

            $("#kobold_order").sortable("enable");
        } else {
            //$('.button').disableSelection();
            preset_settings = "gui";
            $("#range_block").find('input').prop("disabled", true);
            $("#range_block").css("opacity", 0.5);

            $("#kobold-advanced-config").find('input').prop("disabled", true);
            $("#kobold-advanced-config").css('opacity', 0.5);

            $("#kobold_order").sortable("disable");
        }
        saveSettingsDebounced();
    });

    $("#settings_perset_novel").change(function () {
        nai_settings.preset_settings_novel = $("#settings_perset_novel")
            .find(":selected")
            .text();

        const preset = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]];
        loadNovelPreset(preset);
        amount_gen = Number($("#amount_gen").val());
        max_context = Number($("#max_context").val());

        saveSettingsDebounced();
    });

    $("#main_api").change(function () {
        is_get_status = false;
        is_get_status_novel = false;
        setOpenAIOnlineStatus(false);
        online_status = "no_connection";
        checkOnlineStatus();
        changeMainAPI();
        saveSettingsDebounced();
    });

    ////////////////// OPTIMIZED RANGE SLIDER LISTENERS////////////////

    var sliderLocked = true;
    var sliderTimer;

    $("input[type='range']").on("touchstart", function () {
        // Unlock the slider after 300ms
        setTimeout(function () {
            sliderLocked = false;
            $(this).css('background-color', 'var(--SmartThemeQuoteColor)');
        }.bind(this), 300);
    });

    $("input[type='range']").on("touchend", function () {
        clearTimeout(sliderTimer);
        $(this).css('background-color', '');
        sliderLocked = true;
    });

    $("input[type='range']").on("touchmove", function (event) {
        if (sliderLocked) {
            event.preventDefault();
        }
    });

    const sliders = [
        {
            sliderId: "#amount_gen",
            counterId: "#amount_gen_counter",
            format: (val) => `${val}`,
            setValue: (val) => { amount_gen = Number(val); },
        },
        {
            sliderId: "#max_context",
            counterId: "#max_context_counter",
            format: (val) => `${val}`,
            setValue: (val) => { max_context = Number(val); },
        }
    ];

    sliders.forEach(slider => {
        $(document).on("input", slider.sliderId, function () {
            const value = $(this).val();
            const formattedValue = slider.format(value);
            slider.setValue(value);
            $(slider.counterId).val(formattedValue);
            saveSettingsDebounced();
        });
    });

    //////////////////////////////////////////////////////////////

    $("#select_chat_cross").click(function () {
        $("#shadow_select_chat_popup").transition({
            opacity: 0,
            duration: 200,
            easing: animation_easing,
        });
        setTimeout(function () { $("#shadow_select_chat_popup").css("display", "none"); }, 200);
        //$("#shadow_select_chat_popup").css("display", "none");
        $("#load_select_chat_div").css("display", "block");
    });

    if (navigator.clipboard === undefined) {
        // No clipboard support
        $(".mes_copy").remove();
    }
    else {
        $(document).on("pointerup", ".mes_copy", function () {
            if (this_chid !== undefined || selected_group) {
                const message = $(this).closest(".mes");

                if (message.data("isSystem")) {
                    return;
                }
                try {
                    var edit_mes_id = $(this).closest(".mes").attr("mesid");
                    var text = chat[edit_mes_id]["mes"];
                    navigator.clipboard.writeText(text);
                    toastr.info('Copied!', '', { timeOut: 2000 });
                } catch (err) {
                    console.error('Failed to copy: ', err);
                }
            }
        });
    }

    $(document).on("pointerup", ".mes_prompt", function () {
        let mesIdForItemization = $(this).closest('.mes').attr('mesId');
        console.log(`looking for mesID: ${mesIdForItemization}`);
        if (itemizedPrompts.length !== undefined && itemizedPrompts.length !== 0) {
            promptItemize(itemizedPrompts, mesIdForItemization);
        }
    })

    $(document).on("pointerup", "#copyPromptToClipboard", function () {
        let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt;
        let rawPromptValues = rawPrompt;

        if (Array.isArray(rawPrompt)) {
            rawPromptValues = rawPrompt.map(x => x.content).join('\n');
        }

        navigator.clipboard.writeText(rawPromptValues);
        toastr.info('Copied!', '', { timeOut: 2000 });
    });

    $(document).on("pointerup", "#showRawPrompt", function () {
        //console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
        console.log(PromptArrayItemForRawPromptDisplay);
        console.log(itemizedPrompts);
        console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);

        let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt;
        let rawPromptValues = rawPrompt;

        if (Array.isArray(rawPrompt)) {
            rawPromptValues = rawPrompt.map(x => x.content).join('\n');
        }

        //let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '<br>');
        $("#rawPromptWrapper").text(rawPromptValues);
        rawPromptPopper.update();
        $('#rawPromptPopup').toggle();
    })

    //********************
    //***Message Editor***
    $(document).on("click", ".mes_edit", async function () {
        if (this_chid !== undefined || selected_group) {
            // Previously system messages we're allowed to be edited
            /*const message = $(this).closest(".mes");

            if (message.data("isSystem")) {
                return;
            }*/

            let chatScrollPosition = $("#chat").scrollTop();
            if (this_edit_mes_id !== undefined) {
                let mes_edited = $(`#chat [mesid="${this_edit_mes_id}"]`).find(".mes_edit_done");
                if (Number(edit_mes_id) == count_view_mes - 1) { //if the generating swipe (...)
                    if (chat[edit_mes_id]['swipe_id'] !== undefined) {
                        if (chat[edit_mes_id]['swipes'].length === chat[edit_mes_id]['swipe_id']) {
                            run_edit = false;
                        }
                    }
                    if (run_edit) {
                        hideSwipeButtons();
                    }
                }
                await messageEditDone(mes_edited);
            }
            $(this).closest(".mes_block").find(".mes_text").empty();
            $(this).closest(".mes_block").find(".mes_buttons").css("display", "none");
            $(this).closest(".mes_block").find(".mes_edit_buttons").css("display", "inline-flex");
            var edit_mes_id = $(this).closest(".mes").attr("mesid");
            this_edit_mes_id = edit_mes_id;

            var text = chat[edit_mes_id]["mes"];
            if (chat[edit_mes_id]["is_user"]) {
                this_edit_mes_chname = name1;
            } else if (chat[edit_mes_id]["force_avatar"]) {
                this_edit_mes_chname = chat[edit_mes_id]["name"];
            } else {
                this_edit_mes_chname = name2;
            }
            if (power_user.trim_spaces) {
                text = text.trim();
            }
            $(this)
                .closest(".mes_block")
                .find(".mes_text")
                .append(
                    `<textarea id='curEditTextarea' class='edit_textarea' style='max-width:auto;'></textarea>`
                );
            $('#curEditTextarea').val(text);
            let edit_textarea = $(this)
                .closest(".mes_block")
                .find(".edit_textarea");
            edit_textarea.height(0);
            edit_textarea.height(edit_textarea[0].scrollHeight);
            edit_textarea.focus();
            edit_textarea[0].setSelectionRange(     //this sets the cursor at the end of the text
                edit_textarea.val().length,
                edit_textarea.val().length
            );
            if (this_edit_mes_id == count_view_mes - 1) {
                $("#chat").scrollTop(chatScrollPosition);
            }

            updateEditArrowClasses();
        }
    });

    $(document).on('input', '#curEditTextarea', function () {
        if (power_user.auto_save_msg_edits === true) {
            messageEditAuto($(this));
        }
    })

    $(document).on("click", ".extraMesButtonsHint", function (e) {
        const elmnt = e.target;
        $(elmnt).transition({
            opacity: 0,
            duration: 150,
            easing: 'ease-in-out',
        });
        setTimeout(function () {
            $(elmnt).hide();
            $(elmnt).siblings(".extraMesButtons").css('opcacity', '0');
            $(elmnt).siblings(".extraMesButtons").css('display', 'flex');
            $(elmnt).siblings(".extraMesButtons").transition({
                opacity: 1,
                duration: 150,
                easing: 'ease-in-out',
            });
        }, 150);
    })

    $(document).on("click", function (e) {
        // Expanded options don't need to be closed
        if (power_user.expand_message_actions) {
            return;
        }

        // Check if the click was outside the relevant elements
        if (!$(e.target).closest('.extraMesButtons, .extraMesButtonsHint').length) {
            // Transition out the .extraMesButtons first
            $('.extraMesButtons:visible').transition({
                opacity: 0,
                duration: 150,
                easing: 'ease-in-out',
                complete: function () {
                    $(this).hide(); // Hide the .extraMesButtons after the transition

                    // Transition the .extraMesButtonsHint back in
                    $('.extraMesButtonsHint:not(:visible)').show().transition({
                        opacity: .2,
                        duration: 150,
                        easing: 'ease-in-out'
                    });
                }
            });
        }
    });

    $(document).on("click", ".mes_edit_cancel", function () {
        let text = chat[this_edit_mes_id]["mes"];

        $(this).closest(".mes_block").find(".mes_text").empty();
        $(this).closest(".mes_edit_buttons").css("display", "none");
        $(this).closest(".mes_block").find(".mes_buttons").css("display", "");
        $(this)
            .closest(".mes_block")
            .find(".mes_text")
            .append(messageFormatting(
                text,
                this_edit_mes_chname,
                chat[this_edit_mes_id].is_system,
                chat[this_edit_mes_id].is_user,
            ));
        appendImageToMessage(chat[this_edit_mes_id], $(this).closest(".mes"));
        addCopyToCodeBlocks($(this).closest(".mes"));
        this_edit_mes_id = undefined;
    });

    $(document).on("click", ".mes_edit_up", async function () {
        if (is_send_press || this_edit_mes_id <= 0) {
            return;
        }

        hideSwipeButtons();
        const targetId = Number(this_edit_mes_id) - 1;
        const target = $(`#chat .mes[mesid="${targetId}"]`);
        const root = $(this).closest('.mes');

        if (root.length === 0 || target.length === 0) {
            return;
        }

        root.insertBefore(target);

        target.attr("mesid", this_edit_mes_id);
        root.attr("mesid", targetId);

        const temp = chat[targetId];
        chat[targetId] = chat[this_edit_mes_id];
        chat[this_edit_mes_id] = temp;

        this_edit_mes_id = targetId;
        updateViewMessageIds();
        await saveChatConditional();
        showSwipeButtons();
    });

    $(document).on("click", ".mes_edit_down", async function () {
        if (is_send_press || this_edit_mes_id >= chat.length - 1) {
            return;
        }

        hideSwipeButtons();
        const targetId = Number(this_edit_mes_id) + 1;
        const target = $(`#chat .mes[mesid="${targetId}"]`);
        const root = $(this).closest('.mes');

        if (root.length === 0 || target.length === 0) {
            return;
        }

        root.insertAfter(target);

        target.attr("mesid", this_edit_mes_id);
        root.attr("mesid", targetId);

        const temp = chat[targetId];
        chat[targetId] = chat[this_edit_mes_id];
        chat[this_edit_mes_id] = temp;

        this_edit_mes_id = targetId;
        updateViewMessageIds();
        await saveChatConditional();
        showSwipeButtons();
    });

    $(document).on("click", ".mes_edit_copy", async function () {
        const confirmation = await callPopup('Create a copy of this message?', 'confirm');
        if (!confirmation) {
            return;
        }

        hideSwipeButtons();
        let oldScroll = $('#chat')[0].scrollTop;
        const clone = JSON.parse(JSON.stringify(chat[this_edit_mes_id])); // quick and dirty clone
        clone.send_date = Date.now();
        clone.mes = $(this).closest(".mes").find('.edit_textarea').val();

        if (power_user.trim_spaces) {
            clone.mes = clone.mes.trim();
        }

        chat.splice(Number(this_edit_mes_id) + 1, 0, clone);
        addOneMessage(clone, { insertAfter: this_edit_mes_id });

        updateViewMessageIds();
        await saveChatConditional();
        $('#chat')[0].scrollTop = oldScroll;
        showSwipeButtons();
    });

    $(document).on("click", ".mes_edit_delete", async function (event, customData) {
        const fromSlashCommand = customData?.fromSlashCommand || false;
        const swipeExists = (!Array.isArray(chat[this_edit_mes_id].swipes) || chat[this_edit_mes_id].swipes.length <= 1 || chat[this_edit_mes_id].is_user || parseInt(this_edit_mes_id) !== chat.length - 1);
        if (power_user.confirm_message_delete && fromSlashCommand !== true) {
            const confirmation = swipeExists ? await callPopup("Are you sure you want to delete this message?", 'confirm')
                : await callPopup("<h3>Delete this...</h3> <select id='del_type'><option value='swipe'>Swipe</option><option value='message'>Message</option></select>", 'confirm')
            if (!confirmation) {
                return;
            }
        }

        const mes = $(this).closest(".mes");

        if (!mes) {
            return;
        }

        if ($('#del_type').val() === 'swipe') {
            const swipe_id = chat[this_edit_mes_id]['swipe_id'];
            chat[this_edit_mes_id]['swipes'].splice(swipe_id, 1);
            if (swipe_id > 0) {
                $('.swipe_left:last').click();
            } else {
                $('.swipe_right:last').click()
            }
        } else {
            chat.splice(this_edit_mes_id, 1);
            mes.remove();
            count_view_mes--;
        }

        this_edit_mes_id = undefined;

        updateViewMessageIds();
        await saveChatConditional();

        eventSource.emit(event_types.MESSAGE_DELETED, count_view_mes);

        hideSwipeButtons();
        showSwipeButtons();
    });

    $(document).on("click", ".mes_edit_done", async function () {
        await messageEditDone($(this));
    });

    $("#your_name_button").click(function () {
        setUserName($('#your_name').val());
    });

    $('#sync_name_button').on('click', async function () {
        const confirmation = await callPopup(`<h3>Are you sure?</h3>All user-sent messages in this chat will be attributed to ${name1}.`, 'confirm');

        if (!confirmation) {
            return;
        }

        for (const mes of chat) {
            if (mes.is_user) {
                mes.name = name1;
                mes.force_avatar = getUserAvatar(user_avatar);
            }
        }

        await saveChatConditional();
        await reloadCurrentChat();
    });
    //Select chat

    $("#api_button_novel").on('click', async function (e) {
        e.stopPropagation();
        const api_key_novel = String($("#api_key_novel").val()).trim();

        if (api_key_novel.length) {
            await writeSecret(SECRET_KEYS.NOVEL, api_key_novel);
        }

        if (!secret_state[SECRET_KEYS.NOVEL]) {
            console.log('No secret key saved for NovelAI');
            return;
        }

        $("#api_loading_novel").css("display", "inline-block");
        $("#api_button_novel").css("display", "none");
        is_get_status_novel = true;
        is_api_button_press_novel = true;
        // Check near immediately rather than waiting for up to 90s
        setTimeout(getStatusNovel, 10);
    });

    //**************************CHARACTER IMPORT EXPORT*************************//
    $("#character_import_button").click(function () {
        $("#character_import_file").click();
    });

    $("#character_import_file").on("change", function (e) {
        $("#rm_info_avatar").html("");
        if (!e.target.files.length) {
            return;
        }

        for (const file of e.target.files) {
            importCharacter(file);
        }
    });

    $("#export_button").on('click', function (e) {
        $('#export_format_popup').toggle();
        exportPopper.update();
    });

    $(document).on('click', '.export_format', async function () {
        const format = $(this).data('format');

        if (!format) {
            return;
        }

        // Save before exporting
        await createOrEditCharacter();
        const body = { format, avatar_url: characters[this_chid].avatar };

        const response = await fetch('/exportcharacter', {
            method: 'POST',
            headers: getRequestHeaders(),
            body: JSON.stringify(body),
        });

        if (response.ok) {
            const filename = characters[this_chid].avatar.replace('.png', `.${format}`);
            const blob = await response.blob();
            const a = document.createElement("a");
            a.href = URL.createObjectURL(blob);
            a.setAttribute("download", filename);
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
        }


        $('#export_format_popup').hide();
    });
    //**************************CHAT IMPORT EXPORT*************************//
    $("#chat_import_button").click(function () {
        $("#chat_import_file").click();
    });

    $("#chat_import_file").on("change", async function (e) {
        var file = e.target.files[0];

        if (!file) {
            return;
        }

        var ext = file.name.match(/\.(\w+)$/);
        if (
            !ext ||
            (ext[1].toLowerCase() != "json" && ext[1].toLowerCase() != "jsonl")
        ) {
            return;
        }

        if (selected_group && file.name.endsWith('.json')) {
            toastr.warning("Only SillyTavern's own format is supported for group chat imports. Sorry!");
            return;
        }

        var format = ext[1].toLowerCase();
        $("#chat_import_file_type").val(format);

        var formData = new FormData($("#form_import_chat").get(0));
        formData.append('user_name', name1);
        $("#select_chat_div").html("");
        $("#load_select_chat_div").css("display", "block");

        if (selected_group) {
            await importGroupChat(formData);
        } else {
            await importCharacterChat(formData);
        }
    });

    $("#rm_button_group_chats").click(function () {
        selected_button = "group_chats";
        select_group_chats();
    });

    $("#rm_button_back_from_group").click(function () {
        selected_button = "characters";
        select_rm_characters();
    });

    $("#dupe_button").click(async function () {
        await DupeChar();
    });

    $(document).on("click", ".select_chat_block, .bookmark_link, .mes_bookmark", async function () {
        let file_name = $(this).hasClass('mes_bookmark')
            ? $(this).closest('.mes').attr('bookmark_link')
            : $(this).attr("file_name").replace(".jsonl", "");

        if (!file_name) {
            return;
        }

        if (selected_group) {
            await openGroupChat(selected_group, file_name);
        } else {
            await openCharacterChat(file_name);
        }

        $("#shadow_select_chat_popup").css("display", "none");
        $("#load_select_chat_div").css("display", "block");
    });

    $(document).on("click", ".mes_create_bookmark", async function () {
        var selected_mes_id = $(this).closest(".mes").attr("mesid");
        if (selected_mes_id !== undefined) {
            createNewBookmark(selected_mes_id);
        }
    });

    $(document).on("click", ".mes_create_branch", async function () {
        var selected_mes_id = $(this).closest(".mes").attr("mesid");
        if (selected_mes_id !== undefined) {
            branchChat(selected_mes_id);
        }
    });

    $(document).on("click", ".mes_stop", function () {
        if (streamingProcessor) {
            streamingProcessor.abortController.abort();
            streamingProcessor.isStopped = true;
            streamingProcessor.onStopStreaming();
            streamingProcessor = null;
        }
        if (abortController) {
            abortController.abort();
            hideStopButton();
        }
        eventSource.emit(event_types.GENERATION_STOPPED);
    });

    $('.drawer-toggle').on('click', function () {
        var icon = $(this).find('.drawer-icon');
        var drawer = $(this).parent().find('.drawer-content');
        if (drawer.hasClass('resizing')) { return }
        var drawerWasOpenAlready = $(this).parent().find('.drawer-content').hasClass('openDrawer');
        let targetDrawerID = $(this).parent().find('.drawer-content').attr('id');
        const pinnedDrawerClicked = drawer.hasClass('pinnedOpen');

        if (!drawerWasOpenAlready) { //to open the drawer
            $('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, "swing", async function () {
                await delay(50); $(this).closest('.drawer-content').removeClass('resizing');
            });
            $('.openIcon').toggleClass('closedIcon openIcon');
            $('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer');
            icon.toggleClass('openIcon closedIcon');
            drawer.toggleClass('openDrawer closedDrawer');

            //console.log(targetDrawerID);
            if (targetDrawerID === 'right-nav-panel') {
                $(this).closest('.drawer').find('.drawer-content').addClass('resizing').slideToggle({
                    duration: 200,
                    easing: "swing",
                    start: function () {
                        jQuery(this).css('display', 'flex'); //flex needed to make charlist scroll
                    },
                    complete: async function () {
                        favsToHotswap();
                        await delay(50);
                        $(this).closest('.drawer-content').removeClass('resizing');
                        $("#rm_print_characters_block").trigger("scroll");
                    }
                })
            } else {
                $(this).closest('.drawer').find('.drawer-content').addClass('resizing').slideToggle(200, "swing", async function () {
                    await delay(50); $(this).closest('.drawer-content').removeClass('resizing');
                });
            }

            // Set the height of "autoSetHeight" textareas within the drawer to their scroll height
            $(this).closest('.drawer').find('.drawer-content textarea.autoSetHeight').each(function () {
                resetScrollHeight($(this));
            });

        } else if (drawerWasOpenAlready) { //to close manually
            icon.toggleClass('closedIcon openIcon');

            if (pinnedDrawerClicked) {
                $(drawer).addClass('resizing').slideToggle(200, "swing", async function () {
                    await delay(50); $(this).removeClass('resizing');
                });
            }
            else {
                $('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, "swing", async function () {
                    await delay(50); $(this).closest('.drawer-content').removeClass('resizing');
                });
            }

            drawer.toggleClass('closedDrawer openDrawer');
        }
    });

    $("html").on('touchstart mousedown', function (e) {
        var clickTarget = $(e.target);

        if ($('#export_format_popup').is(':visible')
            && clickTarget.closest('#export_button').length == 0
            && clickTarget.closest('#export_format_popup').length == 0) {
            $('#export_format_popup').hide();
        }

        const forbiddenTargets = [
            '#character_cross',
            '#avatar-and-name-block',
            '#shadow_popup',
            '#world_popup',
            '.ui-widget',
            '.text_pole',
            '#toast-container',
            '.select2-results',
        ];
        for (const id of forbiddenTargets) {
            if (clickTarget.closest(id).length > 0) {
                return;
            }
        }

        var targetParentHasOpenDrawer = clickTarget.parents('.openDrawer').length;
        if (clickTarget.hasClass('drawer-icon') == false && !clickTarget.hasClass('openDrawer')) {
            if (jQuery.find('.openDrawer').length !== 0) {
                if (targetParentHasOpenDrawer === 0) {
                    //console.log($('.openDrawer').not('.pinnedOpen').length);
                    $('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, "swing", function () {
                        $(this).closest('.drawer-content').removeClass('resizing')
                    });
                    $('.openIcon').toggleClass('closedIcon openIcon');
                    $('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer');

                }
            }
        }
    });

    $(document).on('click', '.inline-drawer-toggle', function (e) {
        if ($(e.target).hasClass('text_pole')) {
            return;
        };
        var icon = $(this).find('.inline-drawer-icon');
        icon.toggleClass('down up');
        icon.toggleClass('fa-circle-chevron-down fa-circle-chevron-up');
        $(this).closest('.inline-drawer').find('.inline-drawer-content').stop().slideToggle();

        // Set the height of "autoSetHeight" textareas within the inline-drawer to their scroll height
        $(this).closest('.inline-drawer').find('.inline-drawer-content textarea.autoSetHeight').each(function () {
            resetScrollHeight($(this));
        });
    });

    $(document).on('click', '.mes .avatar', function () {

        //console.log(isMobile());
        //console.log($('body').hasClass('waifuMode'));

        /* if (isMobile() === true && !$('body').hasClass('waifuMode')) {
            console.debug('saw mobile regular mode, returning');
            return;
        } else { console.debug('saw valid env for zoomed display') } */

        let thumbURL = $(this).children('img').attr('src');
        let charsPath = '/characters/'
        let targetAvatarImg = thumbURL.substring(thumbURL.lastIndexOf("=") + 1);
        let charname = targetAvatarImg.replace('.png', '');

        // Remove existing zoomed avatars for characters that are not the clicked character when moving UI is not enabled
        if (!power_user.movingUI) {
            $('.zoomed_avatar').each(function () {
                const currentForChar = $(this).attr('forChar');
                if (currentForChar !== charname && typeof currentForChar !== 'undefined') {
                    console.debug(`Removing zoomed avatar for character: ${currentForChar}`);
                    $(this).remove();
                }
            });
        }

        let avatarSrc = isDataURL(thumbURL) ? thumbURL : charsPath + targetAvatarImg;
        if ($(`.zoomed_avatar[forChar="${charname}"]`).length) {
            console.debug('removing container as it already existed')
            $(`.zoomed_avatar[forChar="${charname}"]`).remove();
        } else {
            console.debug('making new container from template')
            const template = $('#zoomed_avatar_template').html();
            const newElement = $(template);
            newElement.attr('forChar', charname);
            newElement.attr('id', `zoomFor_${charname}`);
            newElement.addClass('draggable');
            newElement.find('.drag-grabber').attr('id', `zoomFor_${charname}header`);

            $('body').append(newElement);
            if ($(this).parent().parent().attr('is_user') == 'true') { //handle user avatars
                $(`.zoomed_avatar[forChar="${charname}"] img`).attr('src', thumbURL);
            } else if ($(this).parent().parent().attr('is_system') == 'true') { //handle system avatars
                $(`.zoomed_avatar[forChar="${charname}"] img`).attr('src', thumbURL);
            } else if ($(this).parent().parent().attr('is_user') == 'false') { //handle char avatars
                $(`.zoomed_avatar[forChar="${charname}"] img`).attr('src', avatarSrc);
            }
            loadMovingUIState();
            $(`.zoomed_avatar[forChar="${charname}"]`).css('display', 'block');
            dragElement(newElement)

            $(`.zoomed_avatar[forChar="${charname}"] img`).on('dragstart', (e) => {
                console.log('saw drag on avatar!');
                e.preventDefault();
                return false;
            });
        }
    });

    $(document).on('click', '#OpenAllWIEntries', function () {
        $("#world_popup_entries_list").children().find('.down').click()
    });
    $(document).on('click', '#CloseAllWIEntries', function () {
        $("#world_popup_entries_list").children().find('.up').click()
    });
    $(document).on('click', '.open_alternate_greetings', openAlternateGreetings);
    /* $('#set_character_world').on('click', openCharacterWorldPopup); */

    $(document).keyup(function (e) {
        if (e.key === "Escape") {
            if (power_user.auto_save_msg_edits === false) {
                closeMessageEditor();
                $("#send_textarea").focus();
            }
            if (power_user.auto_save_msg_edits === true) {
                $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).click()
                $("#send_textarea").focus();
            }
            if (!this_edit_mes_id && $('#mes_stop').is(':visible')) {
                $('#mes_stop').trigger('click');
                if (chat.length && Array.isArray(chat[chat.length - 1].swipes) && chat[chat.length - 1].swipe_id == chat[chat.length - 1].swipes.length) {
                    $('.last_mes .swipe_left').trigger('click');
                }
            }
        }
    });

    $("#char-management-dropdown").on('change', async (e) => {
        let target = $(e.target.selectedOptions).attr('id');
        switch (target) {
            case 'set_character_world':
                openCharacterWorldPopup();
                break;
            case 'set_chat_scenario':
                setScenarioOverride();
                break;
            case 'renameCharButton':
                renameCharacter();
                break;
            /*case 'dupe_button':
                DupeChar();
                break;
            case 'export_button':
                $('#export_format_popup').toggle();
                exportPopper.update();
                break;
            */
            case 'import_character_info':
                await importEmbeddedWorldInfo();
                saveCharacterDebounced();
                break;
            /*case 'delete_button':
                popup_type = "del_ch";
                callPopup(`
                        <h3>Delete the character?</h3>
                        <b>THIS IS PERMANENT!<br><br>
                        THIS WILL ALSO DELETE ALL<br>
                        OF THE CHARACTER'S CHAT FILES.<br><br></b>`
                );
                break;*/
            default:
                eventSource.emit('charManagementDropdown', target);
        }
        $("#char-management-dropdown").prop('selectedIndex', 0);
    });

    $(document).on('click', '.mes_img_enlarge', enlargeMessageImage);
    $(document).on('click', '.mes_img_delete', deleteMessageImage);

    $(window).on('beforeunload', () => {
        cancelTtsPlay();
        if (streamingProcessor) {
            console.log('Page reloaded. Aborting streaming...');
            streamingProcessor.abortController.abort();
        }
    });

    $(document).on('input', '.range-block-counter input', function () {
        setTimeout(() => {
            const caretPosition = saveCaretPosition($(this).get(0));
            const myText = $(this).val().trim();
            $(this).val(myText); // trim line breaks and spaces
            const masterSelector = $(this).data('for');
            const masterElement = document.getElementById(masterSelector);

            if (masterElement == null) {
                console.error('Master input element not found for the editable label', masterSelector);
                return;
            }

            const myValue = Number(myText);
            const masterStep = Number(masterElement.getAttribute('step'))
            const masterMin = Number($(masterElement).attr('min'));
            const masterMax = Number($(masterElement).attr('max'));
            const rawStepCompare = myValue / masterStep
            const closestStep = Math.round(rawStepCompare)
            const closestStepRaw = (closestStep) * masterStep

            //if text box val is not a number, reset slider val to its previous and wait for better input
            if (Number.isNaN(myValue)) {
                console.warn('Label input is not a valid number. Resetting the value to match slider', myText);
                $(masterElement).trigger('input');
                restoreCaretPosition($(this).get(0), caretPosition);
                return;
            }

            //if textbox val is less than min, set slider to min
            //PROBLEM: the moment slider gets set to min, textbox also auto-sets to min.
            //if min = 0, this prevents further typing and locks input at 0 unless users pastes
            //a multi-character number which is between min and max. adding delay was necessary.
            if (myValue < masterMin) {
                console.warn('Label input is less than minimum.', myText, '<', masterMin);
                $(masterElement).val(masterMin).trigger('input').trigger('mouseup');
                $(masterElement).val(myValue)
                restoreCaretPosition($(this).get(0), caretPosition);
                return;
            }
            //Same as above but in reverse. Not a problem because max value has multiple
            //characters which can be edited.
            if (myValue > masterMax) {
                console.warn('Label input is more than maximum.', myText, '>', masterMax);
                $(masterElement).val(masterMax).trigger('input').trigger('mouseup');
                $(masterElement).val(myValue)
                restoreCaretPosition($(this).get(0), caretPosition);
                return;
            }

            //round input value to nearest step if between min and max
            if (!(myValue < masterMin) && !(myValue > masterMax)) {
                console.debug(`Label value ${myText} is OK, setting slider to closest step (${closestStepRaw})`);
                $(masterElement).val(closestStepRaw).trigger('input').trigger('mouseup');
                restoreCaretPosition($(this).get(0), caretPosition);
                return;
            }

            restoreCaretPosition($(this).get(0), caretPosition);
        }, 2000);
    })

    $(".user_stats_button").on('click', function () {
        userStatsHandler();
    });

    $('#external_import_button').on('click', async () => {
        const html = `<h3>Enter the URL of the content to import</h3>
        Supported sources:<br>
        <ul class="justifyLeft">
            <li>Chub characters (direct link or id)<br>Example: <tt>Anonymous/example-character</tt></li>
            <li>Chub lorebooks (direct link or id)<br>Example: <tt>lorebooks/bartleby/example-lorebook</tt></li>
            <li>More coming soon...</li>
        <ul>`
        const input = await callPopup(html, 'input');

        if (!input) {
            console.debug('Custom content import cancelled');
            return;
        }

        const url = input.trim();
        console.debug('Custom content import started', url);

        const request = await fetch('/api/content/import', {
            method: 'POST',
            headers: getRequestHeaders(),
            body: JSON.stringify({ url }),
        });

        if (!request.ok) {
            toastr.info(request.statusText, 'Custom content import failed');
            console.error('Custom content import failed', request.status, request.statusText);
            return;
        }

        const data = await request.blob();
        const customContentType = request.headers.get('X-Custom-Content-Type');
        const fileName = request.headers.get('Content-Disposition').split('filename=')[1].replace(/"/g, '');
        const file = new File([data], fileName, { type: data.type });

        switch (customContentType) {
            case 'character':
                await processDroppedFiles([file]);
                break;
            case 'lorebook':
                await importWorldInfo(file);
                break;
            default:
                toastr.warning('Unknown content type');
                console.error('Unknown content type', customContentType);
                break;
        }
    });

    const $dropzone = $(document.body);

    $dropzone.on('dragover', (event) => {
        event.preventDefault();
        event.stopPropagation();
        $dropzone.addClass('dragover');
    });

    $dropzone.on('dragleave', (event) => {
        event.preventDefault();
        event.stopPropagation();
        $dropzone.removeClass('dragover');
    });

    $dropzone.on('drop', async (event) => {
        event.preventDefault();
        event.stopPropagation();
        $dropzone.removeClass('dragover');

        const files = Array.from(event.originalEvent.dataTransfer.files);
        if (!files.length) {
            await importFromURL(event.originalEvent.dataTransfer.items, files);
        }
        await processDroppedFiles(files);
    });


    $("#charListGridToggle").on('click', async () => {
        doCharListDisplaySwitch();
    });

    $("#hideCharPanelAvatarButton").on('click', () => {
        $('#avatar-and-name-block').slideToggle()
    });

    $(document).on('mouseup touchend', '#show_more_messages', () => {
        showMoreMessages();
    });

    // Added here to prevent execution before script.js is loaded and get rid of quirky timeouts
    await firstLoadInit();

    registerDebugFunction('backfillTokenCounts', 'Backfill token counters',
        `Recalculates token counts of all messages in the current chat to refresh the counters.
        Useful when you switch between models that have different tokenizers.
        This is a visual change only. Your chat will be reloaded.`, async () => {
        for (const message of chat) {
            // System messages are not counted
            if (message.is_system) {
                continue;
            }

            if (!message.extra) {
                message.extra = {};
            }

            message.extra.token_count = getTokenCount(message.mes, 0);
        }

        await saveChatConditional();
        await reloadCurrentChat();
    });

    registerDebugFunction('generationTest', 'Send a generation request', 'Generates text using the currently selected API.', async () => {
        const text = prompt('Input text:', 'Hello');
        toastr.info('Working on it...');
        const message = await generateRaw(text, null);
        alert(message);
    });
});
